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:
parent
c61dd82163
commit
5dca9fd4d8
@ -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",
|
||||
|
@ -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('');
|
||||
|
@ -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<typeof getAvailableIcons>[number] | BrandIconNames;
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user