mirror of https://github.com/grafana/grafana
AzureMonitor: Add Azure Resource Graph (#33293)
* Add Azure Resource Graph in Azure Plugin * fix lodash import * Fix mock queries * use "backend" sdk * Address comments * add converter for object type * Query error should cause 400 & apply template var * fix backend test & add documentation * update image * Address comments * marshal body from map * use interpolated query instead of raw query * fix test * filter out empty queries * fix go linting problem * use new query field language name * improve variable tests * add better tests for interpolate variable Co-authored-by: joshhunt <josh@trtr.co> Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>pull/34270/head
parent
4e31169a43
commit
71fd0981ca
@ -0,0 +1,271 @@ |
||||
package azuremonitor |
||||
|
||||
import ( |
||||
"bytes" |
||||
|
||||
"context" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"net/http" |
||||
"net/url" |
||||
"path" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
"github.com/grafana/grafana/pkg/api/pluginproxy" |
||||
"github.com/grafana/grafana/pkg/components/simplejson" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/plugins" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/grafana/grafana/pkg/util/errutil" |
||||
"github.com/opentracing/opentracing-go" |
||||
"golang.org/x/net/context/ctxhttp" |
||||
) |
||||
|
||||
// AzureResourceGraphDatasource calls the Azure Resource Graph API's
|
||||
type AzureResourceGraphDatasource struct { |
||||
httpClient *http.Client |
||||
dsInfo *models.DataSource |
||||
pluginManager plugins.Manager |
||||
cfg *setting.Cfg |
||||
} |
||||
|
||||
// AzureResourceGraphQuery is the query request that is built from the saved values for
|
||||
// from the UI
|
||||
type AzureResourceGraphQuery struct { |
||||
RefID string |
||||
ResultFormat string |
||||
URL string |
||||
Model *simplejson.Json |
||||
InterpolatedQuery string |
||||
} |
||||
|
||||
const argAPIVersion = "2018-09-01-preview" |
||||
const argQueryProviderName = "/providers/Microsoft.ResourceGraph/resources" |
||||
|
||||
// executeTimeSeriesQuery does the following:
|
||||
// 1. builds the AzureMonitor url and querystring for each query
|
||||
// 2. executes each query by calling the Azure Monitor API
|
||||
// 3. parses the responses for each query into the timeseries format
|
||||
func (e *AzureResourceGraphDatasource) executeTimeSeriesQuery(ctx context.Context, originalQueries []plugins.DataSubQuery, |
||||
timeRange plugins.DataTimeRange) (backend.QueryDataResponse, error) { |
||||
result := backend.QueryDataResponse{ |
||||
Responses: map[string]backend.DataResponse{}, |
||||
} |
||||
|
||||
queries, err := e.buildQueries(originalQueries, timeRange) |
||||
if err != nil { |
||||
return backend.QueryDataResponse{}, err |
||||
} |
||||
|
||||
for _, query := range queries { |
||||
result.Responses[query.RefID] = e.executeQuery(ctx, query, timeRange) |
||||
} |
||||
|
||||
return result, nil |
||||
} |
||||
|
||||
func (e *AzureResourceGraphDatasource) buildQueries(queries []plugins.DataSubQuery, |
||||
timeRange plugins.DataTimeRange) ([]*AzureResourceGraphQuery, error) { |
||||
var azureResourceGraphQueries []*AzureResourceGraphQuery |
||||
|
||||
for _, query := range queries { |
||||
queryBytes, err := query.Model.Encode() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to re-encode the Azure Resource Graph query into JSON: %w", err) |
||||
} |
||||
|
||||
queryJSONModel := argJSONQuery{} |
||||
err = json.Unmarshal(queryBytes, &queryJSONModel) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to decode the Azure Resource Graph query object from JSON: %w", err) |
||||
} |
||||
|
||||
azureResourceGraphTarget := queryJSONModel.AzureResourceGraph |
||||
azlog.Debug("AzureResourceGraph", "target", azureResourceGraphTarget) |
||||
|
||||
resultFormat := azureResourceGraphTarget.ResultFormat |
||||
if resultFormat == "" { |
||||
resultFormat = "table" |
||||
} |
||||
|
||||
interpolatedQuery, err := KqlInterpolate(query, timeRange, azureResourceGraphTarget.Query) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
azureResourceGraphQueries = append(azureResourceGraphQueries, &AzureResourceGraphQuery{ |
||||
RefID: query.RefID, |
||||
ResultFormat: resultFormat, |
||||
Model: query.Model, |
||||
InterpolatedQuery: interpolatedQuery, |
||||
}) |
||||
} |
||||
|
||||
return azureResourceGraphQueries, nil |
||||
} |
||||
|
||||
func (e *AzureResourceGraphDatasource) executeQuery(ctx context.Context, query *AzureResourceGraphQuery, |
||||
timeRange plugins.DataTimeRange) backend.DataResponse { |
||||
queryResult := backend.DataResponse{} |
||||
|
||||
params := url.Values{} |
||||
params.Add("api-version", argAPIVersion) |
||||
|
||||
queryResultErrorWithExecuted := func(err error) backend.DataResponse { |
||||
queryResult = backend.DataResponse{Error: err} |
||||
frames := data.Frames{ |
||||
&data.Frame{ |
||||
RefID: query.RefID, |
||||
Meta: &data.FrameMeta{ |
||||
ExecutedQueryString: query.InterpolatedQuery, |
||||
}, |
||||
}, |
||||
} |
||||
queryResult.Frames = frames |
||||
return queryResult |
||||
} |
||||
|
||||
reqBody, err := json.Marshal(map[string]interface{}{ |
||||
"subscriptions": query.Model.Get("subscriptions").MustStringArray(), |
||||
"query": query.InterpolatedQuery, |
||||
}) |
||||
|
||||
if err != nil { |
||||
queryResult.Error = err |
||||
return queryResult |
||||
} |
||||
|
||||
req, err := e.createRequest(ctx, e.dsInfo, reqBody) |
||||
|
||||
if err != nil { |
||||
queryResult.Error = err |
||||
return queryResult |
||||
} |
||||
|
||||
req.URL.Path = path.Join(req.URL.Path, argQueryProviderName) |
||||
req.URL.RawQuery = params.Encode() |
||||
|
||||
span, ctx := opentracing.StartSpanFromContext(ctx, "azure resource graph query") |
||||
span.SetTag("interpolated_query", query.InterpolatedQuery) |
||||
span.SetTag("from", timeRange.From) |
||||
span.SetTag("until", timeRange.To) |
||||
span.SetTag("datasource_id", e.dsInfo.Id) |
||||
span.SetTag("org_id", e.dsInfo.OrgId) |
||||
|
||||
defer span.Finish() |
||||
|
||||
if err := opentracing.GlobalTracer().Inject( |
||||
span.Context(), |
||||
opentracing.HTTPHeaders, |
||||
opentracing.HTTPHeadersCarrier(req.Header)); err != nil { |
||||
return queryResultErrorWithExecuted(err) |
||||
} |
||||
|
||||
azlog.Debug("AzureResourceGraph", "Request ApiURL", req.URL.String()) |
||||
res, err := ctxhttp.Do(ctx, e.httpClient, req) |
||||
if err != nil { |
||||
return queryResultErrorWithExecuted(err) |
||||
} |
||||
|
||||
argResponse, err := e.unmarshalResponse(res) |
||||
if err != nil { |
||||
return queryResultErrorWithExecuted(err) |
||||
} |
||||
|
||||
frame, err := ResponseTableToFrame(&argResponse.Data) |
||||
if err != nil { |
||||
return queryResultErrorWithExecuted(err) |
||||
} |
||||
if frame.Meta == nil { |
||||
frame.Meta = &data.FrameMeta{} |
||||
} |
||||
frame.Meta.ExecutedQueryString = req.URL.RawQuery |
||||
|
||||
queryResult.Frames = data.Frames{frame} |
||||
return queryResult |
||||
} |
||||
|
||||
func (e *AzureResourceGraphDatasource) createRequest(ctx context.Context, dsInfo *models.DataSource, reqBody []byte) (*http.Request, error) { |
||||
u, err := url.Parse(dsInfo.Url) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
u.Path = path.Join(u.Path, "render") |
||||
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBuffer(reqBody)) |
||||
if err != nil { |
||||
azlog.Debug("Failed to create request", "error", err) |
||||
return nil, errutil.Wrap("failed to create request", err) |
||||
} |
||||
|
||||
req.Header.Set("Content-Type", "application/json") |
||||
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion)) |
||||
|
||||
// find plugin
|
||||
plugin := e.pluginManager.GetDataSource(dsInfo.Type) |
||||
if plugin == nil { |
||||
return nil, errors.New("unable to find datasource plugin Azure Monitor") |
||||
} |
||||
cloudName := dsInfo.JsonData.Get("cloudName").MustString("azuremonitor") |
||||
|
||||
argRoute, proxypass, err := e.getPluginRoute(plugin, cloudName) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
pluginproxy.ApplyRoute(ctx, req, proxypass, argRoute, dsInfo, e.cfg) |
||||
|
||||
return req, nil |
||||
} |
||||
|
||||
func (e *AzureResourceGraphDatasource) getPluginRoute(plugin *plugins.DataSourcePlugin, cloudName string) ( |
||||
*plugins.AppPluginRoute, string, error) { |
||||
pluginRouteName := "azureresourcegraph" |
||||
|
||||
switch cloudName { |
||||
case "chinaazuremonitor": |
||||
pluginRouteName = "chinaazureresourcegraph" |
||||
case "govazuremonitor": |
||||
pluginRouteName = "govazureresourcegraph" |
||||
} |
||||
|
||||
var argRoute *plugins.AppPluginRoute |
||||
for _, route := range plugin.Routes { |
||||
if route.Path == pluginRouteName { |
||||
argRoute = route |
||||
break |
||||
} |
||||
} |
||||
|
||||
return argRoute, pluginRouteName, nil |
||||
} |
||||
|
||||
func (e *AzureResourceGraphDatasource) unmarshalResponse(res *http.Response) (AzureResourceGraphResponse, error) { |
||||
body, err := ioutil.ReadAll(res.Body) |
||||
if err != nil { |
||||
return AzureResourceGraphResponse{}, err |
||||
} |
||||
defer func() { |
||||
if err := res.Body.Close(); err != nil { |
||||
azlog.Warn("Failed to close response body", "err", err) |
||||
} |
||||
}() |
||||
|
||||
if res.StatusCode/100 != 2 { |
||||
azlog.Debug("Request failed", "status", res.Status, "body", string(body)) |
||||
return AzureResourceGraphResponse{}, fmt.Errorf("request failed, status: %s, body: %s", res.Status, string(body)) |
||||
} |
||||
|
||||
var data AzureResourceGraphResponse |
||||
d := json.NewDecoder(bytes.NewReader(body)) |
||||
d.UseNumber() |
||||
err = d.Decode(&data) |
||||
if err != nil { |
||||
azlog.Debug("Failed to unmarshal azure resource graph response", "error", err, "status", res.Status, "body", string(body)) |
||||
return AzureResourceGraphResponse{}, err |
||||
} |
||||
|
||||
return data, nil |
||||
} |
||||
@ -0,0 +1,75 @@ |
||||
package azuremonitor |
||||
|
||||
import ( |
||||
"fmt" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/google/go-cmp/cmp" |
||||
"github.com/google/go-cmp/cmp/cmpopts" |
||||
"github.com/grafana/grafana/pkg/components/simplejson" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/plugins" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestBuildingAzureResourceGraphQueries(t *testing.T) { |
||||
datasource := &AzureResourceGraphDatasource{} |
||||
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) |
||||
|
||||
tests := []struct { |
||||
name string |
||||
queryModel []plugins.DataSubQuery |
||||
timeRange plugins.DataTimeRange |
||||
azureResourceGraphQueries []*AzureResourceGraphQuery |
||||
Err require.ErrorAssertionFunc |
||||
}{ |
||||
{ |
||||
name: "Query with macros should be interpolated", |
||||
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{ |
||||
JsonData: simplejson.NewFromAny(map[string]interface{}{}), |
||||
}, |
||||
Model: simplejson.NewFromAny(map[string]interface{}{ |
||||
"queryType": "Azure Resource Graph", |
||||
"azureResourceGraph": map[string]interface{}{ |
||||
"query": "resources | where $__contains(name,'res1','res2')", |
||||
"resultFormat": "table", |
||||
}, |
||||
}), |
||||
RefID: "A", |
||||
}, |
||||
}, |
||||
azureResourceGraphQueries: []*AzureResourceGraphQuery{ |
||||
{ |
||||
RefID: "A", |
||||
ResultFormat: "table", |
||||
URL: "", |
||||
Model: simplejson.NewFromAny(map[string]interface{}{ |
||||
"azureResourceGraph": map[string]interface{}{ |
||||
"query": "resources | where $__contains(name,'res1','res2')", |
||||
"resultFormat": "table", |
||||
}, |
||||
}), |
||||
InterpolatedQuery: "resources | where ['name'] in ('res1','res2')", |
||||
}, |
||||
}, |
||||
Err: require.NoError, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
queries, err := datasource.buildQueries(tt.queryModel, tt.timeRange) |
||||
tt.Err(t, err) |
||||
if diff := cmp.Diff(tt.azureResourceGraphQueries, queries, cmpopts.IgnoreUnexported(simplejson.Json{})); diff != "" { |
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,149 @@ |
||||
import { TemplateSrv } from 'app/features/templating/template_srv'; |
||||
import { backendSrv } from 'app/core/services/backend_srv'; |
||||
import AzureResourceGraphDatasource from './azure_resource_graph_datasource'; |
||||
import { CustomVariableModel, initialVariableModelState, VariableHide } from 'app/features/variables/types'; |
||||
import { initialCustomVariableModelState } from 'app/features/variables/custom/reducer'; |
||||
|
||||
const templateSrv = new TemplateSrv(); |
||||
|
||||
const single: CustomVariableModel = { |
||||
...initialVariableModelState, |
||||
id: 'var1', |
||||
name: 'var1', |
||||
index: 0, |
||||
current: { value: 'var1-foo', text: 'var1-foo', selected: true }, |
||||
options: [{ value: 'var1-foo', text: 'var1-foo', selected: true }], |
||||
multi: false, |
||||
includeAll: false, |
||||
query: '', |
||||
hide: VariableHide.dontHide, |
||||
type: 'custom', |
||||
}; |
||||
|
||||
const multi: CustomVariableModel = { |
||||
...initialVariableModelState, |
||||
id: 'var3', |
||||
name: 'var3', |
||||
index: 2, |
||||
current: { value: ['var3-foo', 'var3-baz'], text: 'var3-foo + var3-baz', selected: true }, |
||||
options: [ |
||||
{ selected: true, value: 'var3-foo', text: 'var3-foo' }, |
||||
{ selected: false, value: 'var3-bar', text: 'var3-bar' }, |
||||
{ selected: true, value: 'var3-baz', text: 'var3-baz' }, |
||||
], |
||||
multi: true, |
||||
includeAll: false, |
||||
query: '', |
||||
hide: VariableHide.dontHide, |
||||
type: 'custom', |
||||
}; |
||||
|
||||
templateSrv.init([single, multi]); |
||||
|
||||
jest.mock('app/core/services/backend_srv'); |
||||
jest.mock('@grafana/runtime', () => ({ |
||||
...((jest.requireActual('@grafana/runtime') as unknown) as object), |
||||
getBackendSrv: () => backendSrv, |
||||
getTemplateSrv: () => templateSrv, |
||||
})); |
||||
|
||||
describe('AzureResourceGraphDatasource', () => { |
||||
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest'); |
||||
|
||||
beforeEach(() => { |
||||
jest.clearAllMocks(); |
||||
datasourceRequestMock.mockImplementation(jest.fn()); |
||||
}); |
||||
|
||||
const ctx: any = {}; |
||||
|
||||
beforeEach(() => { |
||||
ctx.instanceSettings = { |
||||
url: 'http://azureresourcegraphapi', |
||||
}; |
||||
|
||||
ctx.ds = new AzureResourceGraphDatasource(ctx.instanceSettings); |
||||
}); |
||||
|
||||
describe('When applying template variables', () => { |
||||
it('should expand single value template variable', () => { |
||||
const target = { |
||||
azureResourceGraph: { |
||||
query: 'Resources | $var1', |
||||
resultFormat: '', |
||||
}, |
||||
}; |
||||
expect(ctx.ds.applyTemplateVariables(target)).toStrictEqual({ |
||||
azureResourceGraph: { query: 'Resources | var1-foo', resultFormat: 'table' }, |
||||
format: undefined, |
||||
queryType: 'Azure Resource Graph', |
||||
refId: undefined, |
||||
subscriptions: undefined, |
||||
}); |
||||
}); |
||||
|
||||
it('should expand multi value template variable', () => { |
||||
const target = { |
||||
azureResourceGraph: { |
||||
query: 'resources | where $__contains(name, $var3)', |
||||
resultFormat: '', |
||||
}, |
||||
}; |
||||
expect(ctx.ds.applyTemplateVariables(target)).toStrictEqual({ |
||||
azureResourceGraph: { |
||||
query: `resources | where $__contains(name, 'var3-foo','var3-baz')`, |
||||
resultFormat: 'table', |
||||
}, |
||||
format: undefined, |
||||
queryType: 'Azure Resource Graph', |
||||
refId: undefined, |
||||
subscriptions: undefined, |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('When interpolating variables', () => { |
||||
beforeEach(() => { |
||||
ctx.variable = { ...initialCustomVariableModelState }; |
||||
}); |
||||
|
||||
describe('and value is a string', () => { |
||||
it('should return an unquoted value', () => { |
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual('abc'); |
||||
}); |
||||
}); |
||||
|
||||
describe('and value is a number', () => { |
||||
it('should return an unquoted value', () => { |
||||
expect(ctx.ds.interpolateVariable(1000, ctx.variable)).toEqual(1000); |
||||
}); |
||||
}); |
||||
|
||||
describe('and value is an array of strings', () => { |
||||
it('should return comma separated quoted values', () => { |
||||
expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).toEqual("'a','b','c'"); |
||||
}); |
||||
}); |
||||
|
||||
describe('and variable allows multi-value and value is a string', () => { |
||||
it('should return a quoted value', () => { |
||||
ctx.variable.multi = true; |
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'"); |
||||
}); |
||||
}); |
||||
|
||||
describe('and variable contains single quote', () => { |
||||
it('should return a quoted value', () => { |
||||
ctx.variable.multi = true; |
||||
expect(ctx.ds.interpolateVariable("a'bc", ctx.variable)).toEqual("'a'bc'"); |
||||
}); |
||||
}); |
||||
|
||||
describe('and variable allows all and value is a string', () => { |
||||
it('should return a quoted value', () => { |
||||
ctx.variable.includeAll = true; |
||||
expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'"); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,56 @@ |
||||
// eslint-disable-next-line lodash/import-scope
|
||||
import _ from 'lodash'; |
||||
import { AzureMonitorQuery, AzureDataSourceJsonData, AzureQueryType } from '../types'; |
||||
import { ScopedVars } from '@grafana/data'; |
||||
import { getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime'; |
||||
|
||||
export default class AzureResourceGraphDatasource extends DataSourceWithBackend< |
||||
AzureMonitorQuery, |
||||
AzureDataSourceJsonData |
||||
> { |
||||
filterQuery(item: AzureMonitorQuery): boolean { |
||||
return !!item.azureResourceGraph?.query; |
||||
} |
||||
|
||||
applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): Record<string, any> { |
||||
const item = target.azureResourceGraph; |
||||
|
||||
const templateSrv = getTemplateSrv(); |
||||
|
||||
const query = templateSrv.replace(item.query, scopedVars, this.interpolateVariable); |
||||
|
||||
return { |
||||
refId: target.refId, |
||||
format: target.format, |
||||
queryType: AzureQueryType.AzureResourceGraph, |
||||
subscriptions: target.subscriptions, |
||||
azureResourceGraph: { |
||||
resultFormat: 'table', |
||||
query, |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
interpolateVariable(value: string, variable: { multi: any; includeAll: any }) { |
||||
if (typeof value === 'string') { |
||||
if (variable.multi || variable.includeAll) { |
||||
return "'" + value + "'"; |
||||
} else { |
||||
return value; |
||||
} |
||||
} |
||||
|
||||
if (typeof value === 'number') { |
||||
return value; |
||||
} |
||||
|
||||
const quotedValues = _.map(value, (val) => { |
||||
if (typeof value === 'number') { |
||||
return value; |
||||
} |
||||
|
||||
return "'" + val + "'"; |
||||
}); |
||||
return quotedValues.join(','); |
||||
} |
||||
} |
||||
@ -0,0 +1,51 @@ |
||||
import React from 'react'; |
||||
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types'; |
||||
import Datasource from '../../datasource'; |
||||
import { InlineFieldRow } from '@grafana/ui'; |
||||
import SubscriptionField from '../SubscriptionField'; |
||||
import QueryField from './QueryField'; |
||||
|
||||
interface LogsQueryEditorProps { |
||||
query: AzureMonitorQuery; |
||||
datasource: Datasource; |
||||
subscriptionId: string; |
||||
onChange: (newQuery: AzureMonitorQuery) => void; |
||||
variableOptionGroup: { label: string; options: AzureMonitorOption[] }; |
||||
setError: (source: string, error: AzureMonitorErrorish | undefined) => void; |
||||
} |
||||
|
||||
const ArgQueryEditor: React.FC<LogsQueryEditorProps> = ({ |
||||
query, |
||||
datasource, |
||||
subscriptionId, |
||||
variableOptionGroup, |
||||
onChange, |
||||
setError, |
||||
}) => { |
||||
return ( |
||||
<div data-testid="azure-monitor-logs-query-editor"> |
||||
<InlineFieldRow> |
||||
<SubscriptionField |
||||
multiSelect |
||||
query={query} |
||||
datasource={datasource} |
||||
subscriptionId={subscriptionId} |
||||
variableOptionGroup={variableOptionGroup} |
||||
onQueryChange={onChange} |
||||
setError={setError} |
||||
/> |
||||
</InlineFieldRow> |
||||
|
||||
<QueryField |
||||
query={query} |
||||
datasource={datasource} |
||||
subscriptionId={subscriptionId} |
||||
variableOptionGroup={variableOptionGroup} |
||||
onQueryChange={onChange} |
||||
setError={setError} |
||||
/> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default ArgQueryEditor; |
||||
@ -0,0 +1,32 @@ |
||||
import { CodeEditor } from '@grafana/ui'; |
||||
import React, { useCallback } from 'react'; |
||||
import { AzureQueryEditorFieldProps } from '../../types'; |
||||
|
||||
const QueryField: React.FC<AzureQueryEditorFieldProps> = ({ query, onQueryChange }) => { |
||||
const onChange = useCallback( |
||||
(newQuery: string) => { |
||||
onQueryChange({ |
||||
...query, |
||||
azureResourceGraph: { |
||||
...query.azureResourceGraph, |
||||
query: newQuery, |
||||
}, |
||||
}); |
||||
}, |
||||
[onQueryChange, query] |
||||
); |
||||
|
||||
return ( |
||||
<CodeEditor |
||||
value={query.azureResourceGraph.query} |
||||
language="kusto" |
||||
height={200} |
||||
width={1000} |
||||
showMiniMap={false} |
||||
onBlur={onChange} |
||||
onSave={onChange} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export default QueryField; |
||||
@ -0,0 +1 @@ |
||||
export { default } from './ArgQueryEditor'; |
||||
Loading…
Reference in new issue