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",
|
"file-saver": "2.0.2",
|
||||||
"history": "4.10.1",
|
"history": "4.10.1",
|
||||||
"hoist-non-react-statics": "3.3.2",
|
"hoist-non-react-statics": "3.3.2",
|
||||||
|
"immer": "8.0.0",
|
||||||
"immutable": "3.8.2",
|
"immutable": "3.8.2",
|
||||||
"is-hotkey": "0.1.6",
|
"is-hotkey": "0.1.6",
|
||||||
"jquery": "3.5.1",
|
"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 = () => {
|
export const IconsOverview = () => {
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
|
@ -2,149 +2,8 @@ import { ComponentSize } from './size';
|
|||||||
export type IconType = 'mono' | 'default';
|
export type IconType = 'mono' | 'default';
|
||||||
export type IconSize = ComponentSize | 'xl' | 'xxl' | 'xxxl';
|
export type IconSize = ComponentSize | 'xl' | 'xxl' | 'xxxl';
|
||||||
|
|
||||||
export type IconName =
|
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'
|
|
||||||
| '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 = (): IconName[] => [
|
|
||||||
'angle-double-down',
|
'angle-double-down',
|
||||||
'angle-double-right',
|
'angle-double-right',
|
||||||
'angle-down',
|
'angle-down',
|
||||||
@ -227,6 +86,7 @@ export const getAvailableIcons = (): IconName[] => [
|
|||||||
'info-circle',
|
'info-circle',
|
||||||
'key-skeleton-alt',
|
'key-skeleton-alt',
|
||||||
'keyboard',
|
'keyboard',
|
||||||
|
'layer-group',
|
||||||
'line-alt',
|
'line-alt',
|
||||||
'link',
|
'link',
|
||||||
'list-ul',
|
'list-ul',
|
||||||
@ -278,4 +138,8 @@ export const getAvailableIcons = (): IconName[] => [
|
|||||||
'users-alt',
|
'users-alt',
|
||||||
'wrap-text',
|
'wrap-text',
|
||||||
'x',
|
'x',
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
|
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
|
resultFormat = timeSeries
|
||||||
}
|
}
|
||||||
|
|
||||||
urlComponents := map[string]string{}
|
// Handle legacy queries without a Resource
|
||||||
urlComponents["workspace"] = azureLogAnalyticsTarget.Workspace
|
var apiURL string
|
||||||
apiURL := fmt.Sprintf("%s/query", urlComponents["workspace"])
|
if azureLogAnalyticsTarget.Resource != "" {
|
||||||
|
apiURL = fmt.Sprintf("v1%s/query", azureLogAnalyticsTarget.Resource)
|
||||||
|
} else {
|
||||||
|
apiURL = fmt.Sprintf("v1/workspaces/%s/query", azureLogAnalyticsTarget.Workspace)
|
||||||
|
}
|
||||||
|
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
rawQuery, err := KqlInterpolate(query, timeRange, azureLogAnalyticsTarget.Query, "TimeGenerated")
|
rawQuery, err := KqlInterpolate(query, timeRange, azureLogAnalyticsTarget.Query, "TimeGenerated")
|
||||||
|
@ -32,6 +32,45 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
|
|||||||
From: fmt.Sprintf("%v", fromStart.Unix()*1000),
|
From: fmt.Sprintf("%v", fromStart.Unix()*1000),
|
||||||
To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).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{
|
queryModel: []plugins.DataSubQuery{
|
||||||
{
|
{
|
||||||
DataSource: &models.DataSource{
|
DataSource: &models.DataSource{
|
||||||
@ -52,7 +91,7 @@ func TestBuildingAzureLogAnalyticsQueries(t *testing.T) {
|
|||||||
{
|
{
|
||||||
RefID: "A",
|
RefID: "A",
|
||||||
ResultFormat: timeSeries,
|
ResultFormat: timeSeries,
|
||||||
URL: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/query",
|
URL: "v1/workspaces/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/query",
|
||||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||||
"azureLogAnalytics": 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",
|
"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",
|
Path: "loganalyticsazure",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
URL: "https://api.loganalytics.io/v1/workspaces",
|
URL: "https://api.loganalytics.io/",
|
||||||
Headers: []plugins.AppPluginRouteHeader{
|
Headers: []plugins.AppPluginRouteHeader{
|
||||||
{Name: "x-ms-app", Content: "Grafana"},
|
{Name: "x-ms-app", Content: "Grafana"},
|
||||||
},
|
},
|
||||||
@ -100,7 +139,7 @@ func TestPluginRoutes(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Path: "chinaloganalyticsazure",
|
Path: "chinaloganalyticsazure",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
URL: "https://api.loganalytics.azure.cn/v1/workspaces",
|
URL: "https://api.loganalytics.azure.cn/",
|
||||||
Headers: []plugins.AppPluginRouteHeader{
|
Headers: []plugins.AppPluginRouteHeader{
|
||||||
{Name: "x-ms-app", Content: "Grafana"},
|
{Name: "x-ms-app", Content: "Grafana"},
|
||||||
},
|
},
|
||||||
@ -108,7 +147,7 @@ func TestPluginRoutes(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Path: "govloganalyticsazure",
|
Path: "govloganalyticsazure",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
URL: "https://api.loganalytics.us/v1/workspaces",
|
URL: "https://api.loganalytics.us/",
|
||||||
Headers: []plugins.AppPluginRouteHeader{
|
Headers: []plugins.AppPluginRouteHeader{
|
||||||
{Name: "x-ms-app", Content: "Grafana"},
|
{Name: "x-ms-app", Content: "Grafana"},
|
||||||
},
|
},
|
||||||
@ -135,7 +174,7 @@ func TestPluginRoutes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedProxypass: "loganalyticsazure",
|
expectedProxypass: "loganalyticsazure",
|
||||||
expectedRouteURL: "https://api.loganalytics.io/v1/workspaces",
|
expectedRouteURL: "https://api.loganalytics.io/",
|
||||||
Err: require.NoError,
|
Err: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -150,7 +189,7 @@ func TestPluginRoutes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedProxypass: "chinaloganalyticsazure",
|
expectedProxypass: "chinaloganalyticsazure",
|
||||||
expectedRouteURL: "https://api.loganalytics.azure.cn/v1/workspaces",
|
expectedRouteURL: "https://api.loganalytics.azure.cn/",
|
||||||
Err: require.NoError,
|
Err: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -165,7 +204,7 @@ func TestPluginRoutes(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedProxypass: "govloganalyticsazure",
|
expectedProxypass: "govloganalyticsazure",
|
||||||
expectedRouteURL: "https://api.loganalytics.us/v1/workspaces",
|
expectedRouteURL: "https://api.loganalytics.us/",
|
||||||
Err: require.NoError,
|
Err: require.NoError,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -138,6 +138,9 @@ type logJSONQuery struct {
|
|||||||
AzureLogAnalytics struct {
|
AzureLogAnalytics struct {
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
ResultFormat string `json:"resultFormat"`
|
ResultFormat string `json:"resultFormat"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
|
||||||
|
// Deprecated: Queries should be migrated to use Resource instead
|
||||||
Workspace string `json:"workspace"`
|
Workspace string `json:"workspace"`
|
||||||
} `json:"azureLogAnalytics"`
|
} `json:"azureLogAnalytics"`
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,11 @@ export default function createMockDatasource() {
|
|||||||
azureLogAnalyticsDatasource: {
|
azureLogAnalyticsDatasource: {
|
||||||
getKustoSchema: () => Promise.resolve(),
|
getKustoSchema: () => Promise.resolve(),
|
||||||
},
|
},
|
||||||
|
resourcePickerData: {
|
||||||
|
getResourcePickerData: () => ({}),
|
||||||
|
getResourcesForResourceGroup: () => ({}),
|
||||||
|
getResourceURIFromWorkspace: () => '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockDatasource = _mockDatasource as Datasource;
|
const mockDatasource = _mockDatasource as Datasource;
|
||||||
|
@ -49,3 +49,19 @@ export function getAppInsightsApiRoute(azureCloud: string): string {
|
|||||||
throw new Error('The cloud not supported.');
|
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);
|
return this.doRequest(workspaceListUrl, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMetadata(workspace: string) {
|
async getMetadata(resourceUri: string) {
|
||||||
const url = `${this.baseUrl}/${getTemplateSrv().replace(workspace, {})}/metadata`;
|
const url = `${this.baseUrl}/v1${resourceUri}/metadata`;
|
||||||
const resp = await this.doRequest<AzureLogAnalyticsMetadata>(url);
|
|
||||||
|
|
||||||
|
const resp = await this.doRequest<AzureLogAnalyticsMetadata>(url);
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
throw new Error('Unable to get metadata for workspace');
|
throw new Error('Unable to get metadata for workspace');
|
||||||
}
|
}
|
||||||
@ -80,9 +80,9 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getKustoSchema(workspace: string) {
|
async getKustoSchema(resourceUri: string) {
|
||||||
const metadata = await this.getMetadata(workspace);
|
const metadata = await this.getMetadata(resourceUri);
|
||||||
return transformMetadataToKustoSchema(metadata, workspace);
|
return transformMetadataToKustoSchema(metadata, resourceUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): Record<string, any> {
|
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 subscriptionId = templateSrv.replace(target.subscription || this.subscriptionId, scopedVars);
|
||||||
const query = templateSrv.replace(item.query, scopedVars, this.interpolateVariable);
|
const query = templateSrv.replace(item.query, scopedVars, this.interpolateVariable);
|
||||||
|
|
||||||
|
const resource = templateSrv.replace(item.resource, scopedVars);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
refId: target.refId,
|
refId: target.refId,
|
||||||
format: target.format,
|
format: target.format,
|
||||||
@ -106,6 +108,9 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
azureLogAnalytics: {
|
azureLogAnalytics: {
|
||||||
resultFormat: item.resultFormat,
|
resultFormat: item.resultFormat,
|
||||||
query: query,
|
query: query,
|
||||||
|
resource,
|
||||||
|
|
||||||
|
// TODO: Workspace is deprecated and should be migrated to Resources
|
||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -165,6 +170,9 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getWorkspaceDetails(workspaceId: string) {
|
async getWorkspaceDetails(workspaceId: string) {
|
||||||
|
if (!this.subscriptionId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
const response = await this.getWorkspaceList(this.subscriptionId);
|
const response = await this.getWorkspaceList(this.subscriptionId);
|
||||||
|
|
||||||
const details = response.data.value.find((o: any) => {
|
const details = response.data.value.find((o: any) => {
|
||||||
@ -236,7 +244,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
'TimeGenerated'
|
'TimeGenerated'
|
||||||
);
|
);
|
||||||
const querystring = querystringBuilder.generate().uriString;
|
const querystring = querystringBuilder.generate().uriString;
|
||||||
const url = `${this.baseUrl}/${workspace}/query?${querystring}`;
|
const url = `${this.baseUrl}/v1/workspaces/${workspace}/query?${querystring}`;
|
||||||
const queries: any[] = [];
|
const queries: any[] = [];
|
||||||
queries.push({
|
queries.push({
|
||||||
datasourceId: this.id,
|
datasourceId: this.id,
|
||||||
@ -340,6 +348,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: update to be resource-centric
|
||||||
testDatasource(): Promise<any> {
|
testDatasource(): Promise<any> {
|
||||||
const validationError = this.isValidConfig();
|
const validationError = this.isValidConfig();
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
@ -348,7 +357,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
|
|
||||||
return this.getDefaultOrFirstWorkspace()
|
return this.getDefaultOrFirstWorkspace()
|
||||||
.then((ws: any) => {
|
.then((ws: any) => {
|
||||||
const url = `${this.baseUrl}/${ws}/metadata`;
|
const url = `${this.baseUrl}/v1/workspaces/${ws}/metadata`;
|
||||||
|
|
||||||
return this.doRequest(url);
|
return this.doRequest(url);
|
||||||
})
|
})
|
||||||
|
@ -2,10 +2,10 @@ import React from 'react';
|
|||||||
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
|
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
|
||||||
import Datasource from '../../datasource';
|
import Datasource from '../../datasource';
|
||||||
import { InlineFieldRow } from '@grafana/ui';
|
import { InlineFieldRow } from '@grafana/ui';
|
||||||
import SubscriptionField from '../SubscriptionField';
|
|
||||||
import WorkspaceField from './WorkspaceField';
|
|
||||||
import QueryField from './QueryField';
|
import QueryField from './QueryField';
|
||||||
import FormatAsField from './FormatAsField';
|
import FormatAsField from './FormatAsField';
|
||||||
|
import ResourceField from './ResourceField';
|
||||||
|
import useMigrations from './useMigrations';
|
||||||
|
|
||||||
interface LogsQueryEditorProps {
|
interface LogsQueryEditorProps {
|
||||||
query: AzureMonitorQuery;
|
query: AzureMonitorQuery;
|
||||||
@ -24,18 +24,12 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
|
|||||||
onChange,
|
onChange,
|
||||||
setError,
|
setError,
|
||||||
}) => {
|
}) => {
|
||||||
|
useMigrations(datasource, query, onChange);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="azure-monitor-logs-query-editor">
|
<div data-testid="azure-monitor-logs-query-editor">
|
||||||
<InlineFieldRow>
|
<InlineFieldRow>
|
||||||
<SubscriptionField
|
<ResourceField
|
||||||
query={query}
|
|
||||||
datasource={datasource}
|
|
||||||
subscriptionId={subscriptionId}
|
|
||||||
variableOptionGroup={variableOptionGroup}
|
|
||||||
onQueryChange={onChange}
|
|
||||||
setError={setError}
|
|
||||||
/>
|
|
||||||
<WorkspaceField
|
|
||||||
query={query}
|
query={query}
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
subscriptionId={subscriptionId}
|
subscriptionId={subscriptionId}
|
||||||
|
@ -31,8 +31,12 @@ const QueryField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!query.azureLogAnalytics.resource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const promises = [
|
const promises = [
|
||||||
datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.workspace),
|
datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.resource),
|
||||||
getPromise(),
|
getPromise(),
|
||||||
] as const;
|
] 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) => {
|
const handleEditorMount = useCallback((editor: MonacoEditor, monaco: Monaco) => {
|
||||||
monacoPromiseRef.current?.resolve?.({ editor, 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 AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource';
|
||||||
import AppInsightsDatasource from './app_insights/app_insights_datasource';
|
import AppInsightsDatasource from './app_insights/app_insights_datasource';
|
||||||
import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
|
import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
|
||||||
|
import ResourcePickerData from './resourcePicker/resourcePickerData';
|
||||||
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType, InsightsAnalyticsQuery } from './types';
|
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType, InsightsAnalyticsQuery } from './types';
|
||||||
import {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
@ -24,6 +25,7 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
|
|||||||
appInsightsDatasource: AppInsightsDatasource;
|
appInsightsDatasource: AppInsightsDatasource;
|
||||||
azureLogAnalyticsDatasource: AzureLogAnalyticsDatasource;
|
azureLogAnalyticsDatasource: AzureLogAnalyticsDatasource;
|
||||||
insightsAnalyticsDatasource: InsightsAnalyticsDatasource;
|
insightsAnalyticsDatasource: InsightsAnalyticsDatasource;
|
||||||
|
resourcePickerData: ResourcePickerData;
|
||||||
azureResourceGraphDatasource: AzureResourceGraphDatasource;
|
azureResourceGraphDatasource: AzureResourceGraphDatasource;
|
||||||
|
|
||||||
pseudoDatasource: Record<AzureQueryType, DataSourceWithBackend>;
|
pseudoDatasource: Record<AzureQueryType, DataSourceWithBackend>;
|
||||||
@ -39,6 +41,7 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
|
|||||||
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(instanceSettings);
|
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(instanceSettings);
|
||||||
this.insightsAnalyticsDatasource = new InsightsAnalyticsDatasource(instanceSettings);
|
this.insightsAnalyticsDatasource = new InsightsAnalyticsDatasource(instanceSettings);
|
||||||
this.azureResourceGraphDatasource = new AzureResourceGraphDatasource(instanceSettings);
|
this.azureResourceGraphDatasource = new AzureResourceGraphDatasource(instanceSettings);
|
||||||
|
this.resourcePickerData = new ResourcePickerData(instanceSettings);
|
||||||
|
|
||||||
const pseudoDatasource: any = {};
|
const pseudoDatasource: any = {};
|
||||||
pseudoDatasource[AzureQueryType.ApplicationInsights] = this.appInsightsDatasource;
|
pseudoDatasource[AzureQueryType.ApplicationInsights] = this.appInsightsDatasource;
|
||||||
|
@ -183,6 +183,57 @@
|
|||||||
{ "name": "x-ms-app", "content": "Grafana" }
|
{ "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",
|
"path": "workspacesloganalytics",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
@ -237,7 +288,7 @@
|
|||||||
{
|
{
|
||||||
"path": "loganalyticsazure",
|
"path": "loganalyticsazure",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"url": "https://api.loganalytics.io/v1/workspaces",
|
"url": "https://api.loganalytics.io/",
|
||||||
"authType": "azure",
|
"authType": "azure",
|
||||||
"tokenAuth": {
|
"tokenAuth": {
|
||||||
"scopes": ["https://api.loganalytics.io/.default"],
|
"scopes": ["https://api.loganalytics.io/.default"],
|
||||||
@ -257,7 +308,7 @@
|
|||||||
{
|
{
|
||||||
"path": "chinaloganalyticsazure",
|
"path": "chinaloganalyticsazure",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"url": "https://api.loganalytics.azure.cn/v1/workspaces",
|
"url": "https://api.loganalytics.azure.cn/",
|
||||||
"authType": "azure",
|
"authType": "azure",
|
||||||
"tokenAuth": {
|
"tokenAuth": {
|
||||||
"scopes": ["https://api.loganalytics.azure.cn/.default"],
|
"scopes": ["https://api.loganalytics.azure.cn/.default"],
|
||||||
@ -277,7 +328,7 @@
|
|||||||
{
|
{
|
||||||
"path": "govloganalyticsazure",
|
"path": "govloganalyticsazure",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"url": "https://api.loganalytics.us/v1/workspaces",
|
"url": "https://api.loganalytics.us/",
|
||||||
"authType": "azure",
|
"authType": "azure",
|
||||||
"tokenAuth": {
|
"tokenAuth": {
|
||||||
"scopes": ["https://api.loganalytics.us/.default"],
|
"scopes": ["https://api.loganalytics.us/.default"],
|
||||||
|
@ -34,9 +34,9 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
|||||||
reactQueryEditors = [
|
reactQueryEditors = [
|
||||||
AzureQueryType.AzureMonitor,
|
AzureQueryType.AzureMonitor,
|
||||||
AzureQueryType.LogAnalytics,
|
AzureQueryType.LogAnalytics,
|
||||||
|
AzureQueryType.ApplicationInsights,
|
||||||
|
AzureQueryType.InsightsAnalytics,
|
||||||
AzureQueryType.AzureResourceGraph,
|
AzureQueryType.AzureResourceGraph,
|
||||||
// AzureQueryType.ApplicationInsights,
|
|
||||||
// AzureQueryType.InsightsAnalytics,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// target: AzureMonitorQuery;
|
// 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 {
|
export interface AzureLogsQuery {
|
||||||
query: string;
|
query: string;
|
||||||
resultFormat: string;
|
resultFormat: string;
|
||||||
workspace: string;
|
resource?: string;
|
||||||
|
|
||||||
|
/** @deprecated Queries should be migrated to use Resource instead */
|
||||||
|
workspace?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AzureResourceGraphQuery {
|
export interface AzureResourceGraphQuery {
|
||||||
@ -193,3 +196,10 @@ export interface AzureQueryEditorFieldProps {
|
|||||||
onQueryChange: (newQuery: AzureMonitorQuery) => void;
|
onQueryChange: (newQuery: AzureMonitorQuery) => void;
|
||||||
setError: (source: string, error: AzureMonitorErrorish | undefined) => 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"
|
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
|
||||||
integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=
|
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:
|
immer@8.0.1, immer@^8.0.0:
|
||||||
version "8.0.1"
|
version "8.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
|
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
|
||||||
|
Loading…
Reference in New Issue
Block a user