diff --git a/package.json b/package.json index 815609294ba..d97a9dbb84d 100644 --- a/package.json +++ b/package.json @@ -255,6 +255,7 @@ "file-saver": "2.0.2", "history": "4.10.1", "hoist-non-react-statics": "3.3.2", + "immer": "8.0.0", "immutable": "3.8.2", "is-hotkey": "0.1.6", "jquery": "3.5.1", diff --git a/packages/grafana-ui/src/components/Icon/Icon.story.tsx b/packages/grafana-ui/src/components/Icon/Icon.story.tsx index b9a41e68b8d..e0712d2b756 100644 --- a/packages/grafana-ui/src/components/Icon/Icon.story.tsx +++ b/packages/grafana-ui/src/components/Icon/Icon.story.tsx @@ -52,7 +52,8 @@ const IconWrapper = ({ name }: { name: IconName }) => { ); }; -const icons = getAvailableIcons().sort((a, b) => a.localeCompare(b)); +const icons = [...getAvailableIcons()]; +icons.sort((a, b) => a.localeCompare(b)); export const IconsOverview = () => { const [filter, setFilter] = useState(''); diff --git a/packages/grafana-ui/src/types/icon.ts b/packages/grafana-ui/src/types/icon.ts index a9cfd7be313..bdd6f051bcc 100644 --- a/packages/grafana-ui/src/types/icon.ts +++ b/packages/grafana-ui/src/types/icon.ts @@ -2,280 +2,144 @@ import { ComponentSize } from './size'; export type IconType = 'mono' | 'default'; export type IconSize = ComponentSize | 'xl' | 'xxl' | 'xxxl'; -export type IconName = - | 'angle-double-down' - | 'angle-double-right' - | 'angle-down' - | 'angle-left' - | 'angle-right' - | 'angle-up' - | 'apps' - | 'arrow-down' - | 'arrow-from-right' - | 'arrow-left' - | 'arrow-random' - | 'arrow-right' - | 'arrow-up' - | 'arrow' - | 'arrows-h' - | 'bars' - | 'bell-slash' - | 'bell' - | 'bolt' - | 'book-open' - | 'book' - | 'brackets-curly' - | 'bug' - | 'calculator-alt' - | 'calendar-alt' - | 'camera' - | 'channel-add' - | 'chart-line' - | 'check-circle' - | 'check' - | 'circle' - | 'clipboard-alt' - | 'clock-nine' - | 'cloud-download' - | 'cloud-upload' - | 'cloud' - | 'code-branch' - | 'cog' - | 'columns' - | 'comment-alt' - | 'comments-alt' - | 'compass' - | 'copy' - | 'cube' - | 'database' - | 'document-info' - | 'download-alt' - | 'draggabledots' - | 'edit' - | 'ellipsis-v' - | 'envelope' - | 'exchange-alt' - | 'exclamation-triangle' - | 'exclamation' - | 'external-link-alt' - | 'eye-slash' - | 'eye' - | 'fa fa-spinner' - | 'favorite' - | 'file-alt' - | 'file-blank' - | 'file-copy-alt' - | 'filter' - | 'folder-open' - | 'folder-plus' - | 'folder-upload' - | 'folder' - | 'forward' - | 'gf-interpolation-linear' - | 'gf-interpolation-smooth' - | 'gf-interpolation-step-after' - | 'gf-interpolation-step-before' - | 'gf-logs' - | 'github' - | 'gitlab' - | 'grafana' - | 'graph-bar' - | 'google' - | 'heart-break' - | 'heart' - | 'history' - | 'home-alt' - | 'import' - | 'info-circle' - | 'key-skeleton-alt' - | 'keyboard' - | 'line-alt' - | 'link' - | 'list-ul' - | 'lock' - | 'microsoft' - | 'minus-circle' - | 'minus' - | 'mobile-android' - | 'monitor' - | 'okta' - | 'palette' - | 'panel-add' - | 'pause' - | 'pen' - | 'percentage' - | 'play' - | 'plug' - | 'plus-circle' - | 'plus-square' - | 'plus' - | 'power' - | 'question-circle' - | 'repeat' - | 'library-panel' - | 'rocket' - | 'save' - | 'search-minus' - | 'search-plus' - | 'search' - | 'share-alt' - | 'shield' - | 'shield-exclamation' - | 'sign-in-alt' - | 'signal' - | 'signin' - | 'signout' - | 'slack' - | 'sliders-v-alt' - | 'sort-amount-down' - | 'square-shape' - | 'star' - | 'step-backward' - | 'sync' - | 'table' - | 'tag-alt' - | 'times' - | 'trash-alt' - | 'unlock' - | 'upload' - | 'user' - | 'users-alt' - | 'wrap-text' - | 'heart-rate' - | 'x'; +export const getAvailableIcons = () => + [ + 'angle-double-down', + 'angle-double-right', + 'angle-down', + 'angle-left', + 'angle-right', + 'angle-up', + 'apps', + 'arrow-down', + 'arrow-from-right', + 'arrow-left', + 'arrow-random', + 'arrow-right', + 'arrow-up', + 'arrow', + 'arrows-h', + 'bars', + 'bell-slash', + 'bell', + 'bolt', + 'book-open', + 'book', + 'brackets-curly', + 'bug', + 'calculator-alt', + 'calendar-alt', + 'camera', + 'channel-add', + 'chart-line', + 'check-circle', + 'check', + 'circle', + 'clipboard-alt', + 'clock-nine', + 'cloud-download', + 'cloud-upload', + 'cloud', + 'code-branch', + 'cog', + 'columns', + 'comment-alt', + 'comments-alt', + 'compass', + 'copy', + 'cube', + 'database', + 'document-info', + 'download-alt', + 'draggabledots', + 'edit', + 'ellipsis-v', + 'envelope', + 'exchange-alt', + 'exclamation-triangle', + 'external-link-alt', + 'eye-slash', + 'eye', + 'fa fa-spinner', + 'favorite', + 'file-alt', + 'file-blank', + 'file-copy-alt', + 'filter', + 'folder-open', + 'folder-plus', + 'folder-upload', + 'folder', + 'forward', + 'gf-interpolation-linear', + 'gf-interpolation-smooth', + 'gf-interpolation-step-after', + 'gf-interpolation-step-before', + 'gf-logs', + 'grafana', + 'graph-bar', + 'heart-break', + 'heart', + 'history', + 'home-alt', + 'import', + 'info-circle', + 'key-skeleton-alt', + 'keyboard', + 'layer-group', + 'line-alt', + 'link', + 'list-ul', + 'lock', + 'minus-circle', + 'minus', + 'mobile-android', + 'monitor', + 'palette', + 'panel-add', + 'pause', + 'pen', + 'percentage', + 'play', + 'plug', + 'plus-circle', + 'plus-square', + 'plus', + 'power', + 'question-circle', + 'repeat', + 'library-panel', + 'rocket', + 'save', + 'search-minus', + 'search-plus', + 'search', + 'share-alt', + 'shield', + 'shield-exclamation', + 'sign-in-alt', + 'signal', + 'signin', + 'signout', + 'slack', + 'sliders-v-alt', + 'sort-amount-down', + 'square-shape', + 'star', + 'step-backward', + 'sync', + 'table', + 'tag-alt', + 'times', + 'trash-alt', + 'unlock', + 'upload', + 'user', + 'users-alt', + 'wrap-text', + 'x', + ] as const; -export const getAvailableIcons = (): IconName[] => [ - 'angle-double-down', - 'angle-double-right', - 'angle-down', - 'angle-left', - 'angle-right', - 'angle-up', - 'apps', - 'arrow-down', - 'arrow-from-right', - 'arrow-left', - 'arrow-random', - 'arrow-right', - 'arrow-up', - 'arrow', - 'arrows-h', - 'bars', - 'bell-slash', - 'bell', - 'bolt', - 'book-open', - 'book', - 'brackets-curly', - 'bug', - 'calculator-alt', - 'calendar-alt', - 'camera', - 'channel-add', - 'chart-line', - 'check-circle', - 'check', - 'circle', - 'clipboard-alt', - 'clock-nine', - 'cloud-download', - 'cloud-upload', - 'cloud', - 'code-branch', - 'cog', - 'columns', - 'comment-alt', - 'comments-alt', - 'compass', - 'copy', - 'cube', - 'database', - 'document-info', - 'download-alt', - 'draggabledots', - 'edit', - 'ellipsis-v', - 'envelope', - 'exchange-alt', - 'exclamation-triangle', - 'external-link-alt', - 'eye-slash', - 'eye', - 'fa fa-spinner', - 'favorite', - 'file-alt', - 'file-blank', - 'file-copy-alt', - 'filter', - 'folder-open', - 'folder-plus', - 'folder-upload', - 'folder', - 'forward', - 'gf-interpolation-linear', - 'gf-interpolation-smooth', - 'gf-interpolation-step-after', - 'gf-interpolation-step-before', - 'gf-logs', - 'grafana', - 'graph-bar', - 'heart-break', - 'heart', - 'history', - 'home-alt', - 'import', - 'info-circle', - 'key-skeleton-alt', - 'keyboard', - 'line-alt', - 'link', - 'list-ul', - 'lock', - 'minus-circle', - 'minus', - 'mobile-android', - 'monitor', - 'palette', - 'panel-add', - 'pause', - 'pen', - 'percentage', - 'play', - 'plug', - 'plus-circle', - 'plus-square', - 'plus', - 'power', - 'question-circle', - 'repeat', - 'library-panel', - 'rocket', - 'save', - 'search-minus', - 'search-plus', - 'search', - 'share-alt', - 'shield', - 'shield-exclamation', - 'sign-in-alt', - 'signal', - 'signin', - 'signout', - 'slack', - 'sliders-v-alt', - 'sort-amount-down', - 'square-shape', - 'star', - 'step-backward', - 'sync', - 'table', - 'tag-alt', - 'times', - 'trash-alt', - 'unlock', - 'upload', - 'user', - 'users-alt', - 'wrap-text', - 'x', -]; +type BrandIconNames = 'google' | 'microsoft' | 'github' | 'gitlab' | 'okta'; + +export type IconName = ReturnType[number] | BrandIconNames; diff --git a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go b/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go index 8aa2758a010..80fded643ac 100644 --- a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go +++ b/pkg/tsdb/azuremonitor/azure-log-analytics-datasource.go @@ -89,9 +89,13 @@ func (e *AzureLogAnalyticsDatasource) buildQueries(queries []plugins.DataSubQuer resultFormat = timeSeries } - urlComponents := map[string]string{} - urlComponents["workspace"] = azureLogAnalyticsTarget.Workspace - apiURL := fmt.Sprintf("%s/query", urlComponents["workspace"]) + // Handle legacy queries without a Resource + var apiURL string + if azureLogAnalyticsTarget.Resource != "" { + apiURL = fmt.Sprintf("v1%s/query", azureLogAnalyticsTarget.Resource) + } else { + apiURL = fmt.Sprintf("v1/workspaces/%s/query", azureLogAnalyticsTarget.Workspace) + } params := url.Values{} rawQuery, err := KqlInterpolate(query, timeRange, azureLogAnalyticsTarget.Query, "TimeGenerated") diff --git a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource_test.go b/pkg/tsdb/azuremonitor/azure-log-analytics-datasource_test.go index e1ca40e4bff..d46cd8492e6 100644 --- a/pkg/tsdb/azuremonitor/azure-log-analytics-datasource_test.go +++ b/pkg/tsdb/azuremonitor/azure-log-analytics-datasource_test.go @@ -32,6 +32,45 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { From: fmt.Sprintf("%v", fromStart.Unix()*1000), To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), }, + queryModel: []plugins.DataSubQuery{ + { + DataSource: &models.DataSource{ + JsonData: simplejson.NewFromAny(map[string]interface{}{}), + }, + Model: simplejson.NewFromAny(map[string]interface{}{ + "queryType": "Azure Log Analytics", + "azureLogAnalytics": map[string]interface{}{ + "resource": "/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace", + "query": "query=Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", + "resultFormat": timeSeries, + }, + }), + RefID: "A", + }, + }, + azureLogAnalyticsQueries: []*AzureLogAnalyticsQuery{ + { + RefID: "A", + ResultFormat: timeSeries, + URL: "v1/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.OperationalInsights/workspaces/AppInsightsTestDataWorkspace/query", + Model: simplejson.NewFromAny(map[string]interface{}{ + "azureLogAnalytics": map[string]interface{}{ + "query": "query=Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", + "resultFormat": timeSeries, + "workspace": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + }, + }), + Params: url.Values{"query": {"query=Perf | where ['TimeGenerated'] >= datetime('2018-03-15T13:00:00Z') and ['TimeGenerated'] <= datetime('2018-03-15T13:34:00Z') | where ['Computer'] in ('comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, 34000ms), Computer"}}, + Target: "query=query%3DPerf+%7C+where+%5B%27TimeGenerated%27%5D+%3E%3D+datetime%28%272018-03-15T13%3A00%3A00Z%27%29+and+%5B%27TimeGenerated%27%5D+%3C%3D+datetime%28%272018-03-15T13%3A34%3A00Z%27%29+%7C+where+%5B%27Computer%27%5D+in+%28%27comp1%27%2C%27comp2%27%29+%7C+summarize+avg%28CounterValue%29+by+bin%28TimeGenerated%2C+34000ms%29%2C+Computer", + }, + }, + Err: require.NoError, + }, { + name: "Legacy workspace queries should use workspace query endpoint", + timeRange: plugins.DataTimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, queryModel: []plugins.DataSubQuery{ { DataSource: &models.DataSource{ @@ -52,7 +91,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) { { RefID: "A", ResultFormat: timeSeries, - URL: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/query", + URL: "v1/workspaces/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/query", Model: simplejson.NewFromAny(map[string]interface{}{ "azureLogAnalytics": map[string]interface{}{ "query": "query=Perf | where $__timeFilter() | where $__contains(Computer, 'comp1','comp2') | summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer", @@ -92,7 +131,7 @@ func TestPluginRoutes(t *testing.T) { { Path: "loganalyticsazure", Method: "GET", - URL: "https://api.loganalytics.io/v1/workspaces", + URL: "https://api.loganalytics.io/", Headers: []plugins.AppPluginRouteHeader{ {Name: "x-ms-app", Content: "Grafana"}, }, @@ -100,7 +139,7 @@ func TestPluginRoutes(t *testing.T) { { Path: "chinaloganalyticsazure", Method: "GET", - URL: "https://api.loganalytics.azure.cn/v1/workspaces", + URL: "https://api.loganalytics.azure.cn/", Headers: []plugins.AppPluginRouteHeader{ {Name: "x-ms-app", Content: "Grafana"}, }, @@ -108,7 +147,7 @@ func TestPluginRoutes(t *testing.T) { { Path: "govloganalyticsazure", Method: "GET", - URL: "https://api.loganalytics.us/v1/workspaces", + URL: "https://api.loganalytics.us/", Headers: []plugins.AppPluginRouteHeader{ {Name: "x-ms-app", Content: "Grafana"}, }, @@ -135,7 +174,7 @@ func TestPluginRoutes(t *testing.T) { }, }, expectedProxypass: "loganalyticsazure", - expectedRouteURL: "https://api.loganalytics.io/v1/workspaces", + expectedRouteURL: "https://api.loganalytics.io/", Err: require.NoError, }, { @@ -150,7 +189,7 @@ func TestPluginRoutes(t *testing.T) { }, }, expectedProxypass: "chinaloganalyticsazure", - expectedRouteURL: "https://api.loganalytics.azure.cn/v1/workspaces", + expectedRouteURL: "https://api.loganalytics.azure.cn/", Err: require.NoError, }, { @@ -165,7 +204,7 @@ func TestPluginRoutes(t *testing.T) { }, }, expectedProxypass: "govloganalyticsazure", - expectedRouteURL: "https://api.loganalytics.us/v1/workspaces", + expectedRouteURL: "https://api.loganalytics.us/", Err: require.NoError, }, } diff --git a/pkg/tsdb/azuremonitor/types.go b/pkg/tsdb/azuremonitor/types.go index 77d35d3678f..456402c04b3 100644 --- a/pkg/tsdb/azuremonitor/types.go +++ b/pkg/tsdb/azuremonitor/types.go @@ -138,7 +138,10 @@ type logJSONQuery struct { AzureLogAnalytics struct { Query string `json:"query"` ResultFormat string `json:"resultFormat"` - Workspace string `json:"workspace"` + Resource string `json:"resource"` + + // Deprecated: Queries should be migrated to use Resource instead + Workspace string `json:"workspace"` } `json:"azureLogAnalytics"` } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts index 63ccbc3595f..efeb84e50f8 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts @@ -34,6 +34,11 @@ export default function createMockDatasource() { azureLogAnalyticsDatasource: { getKustoSchema: () => Promise.resolve(), }, + resourcePickerData: { + getResourcePickerData: () => ({}), + getResourcesForResourceGroup: () => ({}), + getResourceURIFromWorkspace: () => '', + }, }; const mockDatasource = _mockDatasource as Datasource; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/api/routes.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/api/routes.ts index 587e2106343..89a8e4ef65c 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/api/routes.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/api/routes.ts @@ -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.'); + } +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index 5e6093b1c9b..d7664c8798d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -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(url); + async getMetadata(resourceUri: string) { + const url = `${this.baseUrl}/v1${resourceUri}/metadata`; + const resp = await this.doRequest(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 { @@ -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 { 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); }) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx index e08f3727953..a8202544325 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -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 = ({ onChange, setError, }) => { + useMigrations(datasource, query, onChange); + return (
- - = ({ 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 = ({ 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 }); 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 new file mode 100644 index 00000000000..7b5799a7d35 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx @@ -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 = ({ 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 ( + <> + + + + + + + + + ); +}; + +interface FormattedResourceProps { + resource: AzureResourceSummaryItem; +} + +const FormattedResource: React.FC = ({ resource }) => { + return ( + + {resource.subscriptionName} + + {resource.resourceGroupName} + + {resource.name} + + ); +}; + +const Separator = () => ( + <> + + {'/'} + + +); + +export default ResourceField; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/useMigrations.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/useMigrations.ts new file mode 100644 index 00000000000..6484eee4b8e --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/useMigrations.ts @@ -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]); +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedResourceTable.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedResourceTable.tsx new file mode 100644 index 00000000000..3f0927e0893 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedResourceTable.tsx @@ -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; + onRowSelectedChange: (row: Row, selected: boolean) => void; +} + +const NestedResourceTable: React.FC = ({ + rows, + selectedRows, + noHeader, + requestNestedRows, + onRowSelectedChange, +}) => { + const styles = useStyles2(getStyles); + + return ( + <> + + {!noHeader && ( + + + + + + + + )} +
ScopeTypeLocation
+ +
+ + + + +
+
+ + ); +}; + +export default NestedResourceTable; 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 new file mode 100644 index 00000000000..08ff9b80c3e --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRows.tsx @@ -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; + onRowSelectedChange: (row: Row, selected: boolean) => void; +} + +const NestedRows: React.FC = ({ + rows, + selectedRows, + level, + requestNestedRows, + onRowSelectedChange, +}) => ( + <> + {Object.keys(rows).map((rowId) => ( + + ))} + +); + +interface NestedRowProps { + row: Row; + level: number; + selectedRows: RowGroup; + requestNestedRows: (row: Row) => Promise; + onRowSelectedChange: (row: Row, selected: boolean) => void; +} + +const NestedRow: React.FC = ({ 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 ( + <> + + + + + + {row.typeLabel} + + {row.location ?? '-'} + + + {isOpen && row.children && Object.keys(row.children).length > 0 && ( + + )} + + {openStatus === 'loading' && ( + + + Loading... + + + )} + + ); +}; + +interface EntryIconProps { + entry: Row; + isOpen: boolean; +} + +const EntryIcon: React.FC = ({ isOpen, entry: { type } }) => { + switch (type) { + case EntryType.Collection: + return ; + + case EntryType.SubCollection: + return ; + + case EntryType.Resource: + return ; + + 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 = ({ + 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) => { + const isSelected = ev.target.checked; + onSelectedChange(entry, isSelected); + }, + [entry, onSelectedChange] + ); + + return ( +
+ + {/* 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 && ( + + )} + + {isSelectable && } + + + + {entry.name} + +
+ ); +}; + +export default NestedRows; 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 new file mode 100644 index 00000000000..2ed144365d9 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/index.tsx @@ -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; + 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({}); + const [internalSelected, setInternalSelected] = useState(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 ( +
+ + +
+ {internalSelected && ( + <> + +
Selection
+ + + )} + + + + + + +
+
+ ); +}; + +export default ResourcePicker; + +const getStyles = (theme: GrafanaTheme2) => ({ + selectionFooter: css({ + position: 'sticky', + bottom: 0, + background: theme.colors.background.primary, + paddingTop: theme.spacing(2), + }), +}); 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 new file mode 100644 index 00000000000..0fa539b2b50 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/styles.ts @@ -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; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/types.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/types.ts new file mode 100644 index 00000000000..c0396fff645 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/types.ts @@ -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; +} 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 new file mode 100644 index 00000000000..da334751f10 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts @@ -0,0 +1,12 @@ +const RESOURCE_URI_REGEX = /\/subscriptions\/(?.+)\/resourceGroups\/(?.+)\/providers.+\/(?[\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 }; +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts index 966b539689a..2a011235e34 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -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; @@ -39,6 +41,7 @@ export default class Datasource extends DataSourceApi { + 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( + `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(` + 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(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(` + 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( + query: string, + maxRetries = 1 + ): Promise>> { + try { + return await getBackendSrv() + .fetch>({ + 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; + } + } +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/supportedResources.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/supportedResources.ts new file mode 100644 index 00000000000..638c8b36709 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/supportedResources.ts @@ -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(','); 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 991460b4e1d..153c82be488 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 @@ -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; +} diff --git a/yarn.lock b/yarn.lock index bcfcbbf38d1..78896a7d0de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12304,6 +12304,11 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= +immer@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.0.tgz#08763549ba9dd7d5e2eb4bec504a8315bd9440c2" + integrity sha512-jm87NNBAIG4fHwouilCHIecFXp5rMGkiFrAuhVO685UnMAlOneEAnOyzPt8OnP47TC11q/E7vpzZe0WvwepFTg== + immer@8.0.1, immer@^8.0.0: version "8.0.1" resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"