mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AzureMonitor: Migrate Metrics query editor to React (#30783)
* AzureMonitor: Remove anys from datasource to get the inferred type * AzureMonitor: Cast some datasource types TODO: we want proper types for these * AzureMonitor: Initial react Metrics editor components * start dimension fields * replace replaceTemplateVariable with datasource.replace, and rename onQueryChange to onChange * actually just do template variable replacement in the datasource * don't use azureMonitorIsConfigured * Refactors, mainly around the metric metadata - Convert all the metric metadata options for the Select before its set into state - Stop using SelectableValue because it's basically any when all the properties are optional - the onChange function passed to the fields now just accepts the direct value, rather than wrapped in a SelectableValue * added proper fields, and adding and removing for DimensionFields * Update query with Dimension changes * Width * subscription and query type fields * Should be feature complete now, more or less * fix missing import * fix lint issues * set default subscription ID * Starting to write some tests * tests for query editor * Remove subscription ID from the label in Metrics But we keep it there for the angular stuff * MetricsQueryEditor tests * Update index.test.tsx * fix tests * add template variables to dropdowns * clean up * update tests * Reorganise react components * Group query fields into rows * Rename Option type, add Azure response type * Refactor Metrics metric metadata - Types the Azure API - Moves default metadata values into datasource * nit * update test
This commit is contained in:
parent
05d6d32f3e
commit
13a47aede2
@ -23,6 +23,8 @@ import { HelpModal } from './components/help/HelpModal';
|
||||
import { Footer } from './components/Footer/Footer';
|
||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||
import { SearchField, SearchResults, SearchResultsFilter } from '../features/search';
|
||||
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
|
||||
import QueryEditor from 'app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor';
|
||||
|
||||
const { SecretFormField } = LegacyForms;
|
||||
|
||||
@ -181,4 +183,22 @@ export function registerAngularDirectives() {
|
||||
['onLoad', { watchDepth: 'reference', wrapApply: true }],
|
||||
['onChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
]);
|
||||
|
||||
react2AngularDirective('timePickerSettings', TimePickerSettings, [
|
||||
'renderCount',
|
||||
'refreshIntervals',
|
||||
'timePickerHidden',
|
||||
'nowDelay',
|
||||
'timezone',
|
||||
['onTimeZoneChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
['onRefreshIntervalChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
['onNowDelayChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
['onHideTimePickerChange', { watchDepth: 'reference', wrapApply: true }],
|
||||
]);
|
||||
|
||||
react2AngularDirective('azureMonitorQueryEditor', QueryEditor, [
|
||||
'query',
|
||||
['datasource', { watchDepth: 'reference' }],
|
||||
'onChange',
|
||||
]);
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
import Datasource from '../datasource';
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
};
|
||||
|
||||
export default function createMockDatasource() {
|
||||
// We make this a partial so we get _some_ kind of type safety when making this, rather than
|
||||
// having it be any or casted immediately to Datasource
|
||||
const _mockDatasource: DeepPartial<Datasource> = {
|
||||
getVariables: jest.fn().mockReturnValueOnce([]),
|
||||
|
||||
azureMonitorDatasource: {
|
||||
isConfigured() {
|
||||
return true;
|
||||
},
|
||||
getSubscriptions: jest.fn().mockResolvedValueOnce([]),
|
||||
},
|
||||
|
||||
getResourceGroups: jest.fn().mockResolvedValueOnce([]),
|
||||
getMetricDefinitions: jest.fn().mockResolvedValueOnce([]),
|
||||
getResourceNames: jest.fn().mockResolvedValueOnce([]),
|
||||
getMetricNamespaces: jest.fn().mockResolvedValueOnce([]),
|
||||
getMetricNames: jest.fn().mockResolvedValueOnce([]),
|
||||
getMetricMetadata: jest.fn().mockResolvedValueOnce({
|
||||
primaryAggType: 'average',
|
||||
supportedAggTypes: [],
|
||||
supportedTimeGrains: [],
|
||||
dimensions: [],
|
||||
}),
|
||||
};
|
||||
|
||||
const mockDatasource = _mockDatasource as Datasource;
|
||||
|
||||
return mockDatasource;
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { AzureMonitorQuery, AzureQueryType } from '../types';
|
||||
|
||||
const azureMonitorQuery: AzureMonitorQuery = {
|
||||
appInsights: undefined, // The actualy shape of this at runtime disagrees with the ts interface
|
||||
|
||||
azureLogAnalytics: {
|
||||
query:
|
||||
'//change this example to create your own time series query\n<table name> //the table to query (e.g. Usage, Heartbeat, Perf)\n| where $__timeFilter(TimeGenerated) //this is a macro used to show the full chart’s time range, choose the datetime column here\n| summarize count() by <group by column>, bin(TimeGenerated, $__interval) //change “group by column” to a column in your table, such as “Computer”. The $__interval macro is used to auto-select the time grain. Can also use 1h, 5m etc.\n| order by TimeGenerated asc',
|
||||
resultFormat: 'time_series',
|
||||
workspace: 'e3fe4fde-ad5e-4d60-9974-e2f3562ffdf2',
|
||||
},
|
||||
|
||||
azureMonitor: {
|
||||
// aggOptions: [],
|
||||
aggregation: 'Average',
|
||||
allowedTimeGrainsMs: [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||
// dimensionFilter: '*',
|
||||
dimensionFilters: [],
|
||||
metricDefinition: 'Microsoft.Compute/virtualMachines',
|
||||
metricName: 'Metric A',
|
||||
metricNamespace: 'Microsoft.Compute/virtualMachines',
|
||||
resourceGroup: 'grafanastaging',
|
||||
resourceName: 'grafana',
|
||||
timeGrain: 'auto',
|
||||
alias: '',
|
||||
// timeGrains: [],
|
||||
top: '10',
|
||||
},
|
||||
|
||||
insightsAnalytics: {
|
||||
query: '',
|
||||
resultFormat: 'time_series',
|
||||
},
|
||||
|
||||
queryType: AzureQueryType.AzureMonitor,
|
||||
refId: 'A',
|
||||
subscription: 'abc-123',
|
||||
|
||||
format: 'dunno lol', // unsure what this value should be. It's not there at runtime, but it's in the ts interface
|
||||
};
|
||||
|
||||
export default azureMonitorQuery;
|
@ -20,9 +20,16 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
url: string;
|
||||
baseUrl: string;
|
||||
applicationId: string;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* TODO: Which one of these values should be used? Was there a migration?
|
||||
* */
|
||||
logAnalyticsSubscriptionId: string;
|
||||
subscriptionId: string;
|
||||
|
||||
azureMonitorUrl: string;
|
||||
defaultOrFirstWorkspace: string;
|
||||
subscriptionId: string;
|
||||
cache: Map<string, any>;
|
||||
|
||||
constructor(private instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>) {
|
||||
|
@ -105,9 +105,9 @@ describe('AzureMonitorDatasource', () => {
|
||||
it('should return a list of subscriptions', () => {
|
||||
return ctx.ds.metricFindQuery('subscriptions()').then((results: Array<{ text: string; value: string }>) => {
|
||||
expect(results.length).toBe(2);
|
||||
expect(results[0].text).toBe('Primary - sub1');
|
||||
expect(results[0].text).toBe('Primary');
|
||||
expect(results[0].value).toBe('sub1');
|
||||
expect(results[1].text).toBe('Secondary - sub2');
|
||||
expect(results[1].text).toBe('Secondary');
|
||||
expect(results[1].value).toBe('sub2');
|
||||
});
|
||||
});
|
||||
@ -545,7 +545,7 @@ describe('AzureMonitorDatasource', () => {
|
||||
it('should return list of Resource Groups', () => {
|
||||
return ctx.ds.getSubscriptions().then((results: Array<{ text: string; value: string }>) => {
|
||||
expect(results.length).toEqual(1);
|
||||
expect(results[0].text).toEqual('Primary Subscription - 99999999-cccc-bbbb-aaaa-9106972f9572');
|
||||
expect(results[0].text).toEqual('Primary Subscription');
|
||||
expect(results[0].value).toEqual('99999999-cccc-bbbb-aaaa-9106972f9572');
|
||||
});
|
||||
});
|
||||
@ -856,10 +856,10 @@ describe('AzureMonitorDatasource', () => {
|
||||
'default',
|
||||
'UsedCapacity'
|
||||
)
|
||||
.then((results: any) => {
|
||||
.then((results) => {
|
||||
expect(results.primaryAggType).toEqual('Total');
|
||||
expect(results.supportedAggTypes.length).toEqual(6);
|
||||
expect(results.supportedTimeGrains.length).toEqual(4);
|
||||
expect(results.supportedTimeGrains.length).toEqual(5); // 4 time grains from the API + auto
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -934,15 +934,15 @@ describe('AzureMonitorDatasource', () => {
|
||||
expect(results.dimensions).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"text": "Response type",
|
||||
"label": "Response type",
|
||||
"value": "ResponseType",
|
||||
},
|
||||
Object {
|
||||
"text": "Geo type",
|
||||
"label": "Geo type",
|
||||
"value": "GeoType",
|
||||
},
|
||||
Object {
|
||||
"text": "API name",
|
||||
"label": "API name",
|
||||
"value": "ApiName",
|
||||
},
|
||||
]
|
||||
|
@ -9,9 +9,10 @@ import {
|
||||
AzureMonitorMetricDefinitionsResponse,
|
||||
AzureMonitorResourceGroupsResponse,
|
||||
AzureQueryType,
|
||||
AzureMonitorMetricsMetadataResponse,
|
||||
} from '../types';
|
||||
import { DataSourceInstanceSettings, ScopedVars, MetricFindValue } from '@grafana/data';
|
||||
import { getBackendSrv, DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime';
|
||||
import { getBackendSrv, DataSourceWithBackend, getTemplateSrv, FetchResponse } from '@grafana/runtime';
|
||||
|
||||
const defaultDropdownValue = 'select';
|
||||
|
||||
@ -224,7 +225,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
.then((result: AzureMonitorMetricDefinitionsResponse) => {
|
||||
return ResponseParser.parseResponseValues(result, 'type', 'type');
|
||||
})
|
||||
.then((result: any) => {
|
||||
.then((result) => {
|
||||
return filter(result, (t) => {
|
||||
for (let i = 0; i < this.supportedMetricNamespaces.length; i++) {
|
||||
if (t.value.toLowerCase() === this.supportedMetricNamespaces[i].toLowerCase()) {
|
||||
@ -235,7 +236,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
return false;
|
||||
});
|
||||
})
|
||||
.then((result: any) => {
|
||||
.then((result) => {
|
||||
let shouldHardcodeBlobStorage = false;
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
if (result[i].value === 'Microsoft.Storage/storageAccounts') {
|
||||
@ -340,8 +341,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
this.apiVersion
|
||||
);
|
||||
|
||||
return this.doRequest(url).then((result: any) => {
|
||||
return ResponseParser.parseMetadata(result, metricName);
|
||||
return this.doRequest<AzureMonitorMetricsMetadataResponse>(url).then((result) => {
|
||||
return ResponseParser.parseMetadata(result.data, metricName);
|
||||
});
|
||||
}
|
||||
|
||||
@ -400,15 +401,15 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
return field && field.length > 0;
|
||||
}
|
||||
|
||||
doRequest(url: string, maxRetries = 1): Promise<any> {
|
||||
doRequest<T = any>(url: string, maxRetries = 1): Promise<FetchResponse<T>> {
|
||||
return getBackendSrv()
|
||||
.datasourceRequest({
|
||||
.datasourceRequest<T>({
|
||||
url: this.url + url,
|
||||
method: 'GET',
|
||||
})
|
||||
.catch((error: any) => {
|
||||
if (maxRetries > 0) {
|
||||
return this.doRequest(url, maxRetries - 1);
|
||||
return this.doRequest<T>(url, maxRetries - 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
@ -1,5 +1,11 @@
|
||||
import _ from 'lodash';
|
||||
import TimeGrainConverter from '../time_grain_converter';
|
||||
import {
|
||||
AzureMonitorLocalizedValue,
|
||||
AzureMonitorMetricAvailabilityMetadata,
|
||||
AzureMonitorMetricsMetadataResponse,
|
||||
AzureMonitorOption,
|
||||
} from '../types';
|
||||
export default class ResponseParser {
|
||||
static parseResponseValues(
|
||||
result: any,
|
||||
@ -45,10 +51,11 @@ export default class ResponseParser {
|
||||
return list;
|
||||
}
|
||||
|
||||
static parseMetadata(result: any, metricName: string) {
|
||||
static parseMetadata(result: AzureMonitorMetricsMetadataResponse, metricName: string) {
|
||||
const defaultAggTypes = ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'];
|
||||
const metricData = result?.value.find((v) => v.name.value === metricName);
|
||||
|
||||
if (!result) {
|
||||
if (!metricData) {
|
||||
return {
|
||||
primaryAggType: '',
|
||||
supportedAggTypes: defaultAggTypes,
|
||||
@ -57,20 +64,21 @@ export default class ResponseParser {
|
||||
};
|
||||
}
|
||||
|
||||
const metricData: any = _.find(result.data.value, (o) => {
|
||||
return _.get(o, 'name.value') === metricName;
|
||||
});
|
||||
|
||||
return {
|
||||
primaryAggType: metricData.primaryAggregationType,
|
||||
supportedAggTypes: metricData.supportedAggregationTypes || defaultAggTypes,
|
||||
supportedTimeGrains: ResponseParser.parseTimeGrains(metricData.metricAvailabilities || []),
|
||||
dimensions: ResponseParser.parseDimensions(metricData),
|
||||
|
||||
supportedTimeGrains: [
|
||||
{ label: 'Auto', value: 'auto' },
|
||||
...ResponseParser.parseTimeGrains(metricData.metricAvailabilities ?? []),
|
||||
],
|
||||
dimensions: ResponseParser.parseDimensions(metricData.dimensions ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
static parseTimeGrains(metricAvailabilities: any[]): Array<{ text: string; value: string }> {
|
||||
const timeGrains: any[] = [];
|
||||
static parseTimeGrains(metricAvailabilities: AzureMonitorMetricAvailabilityMetadata[]): AzureMonitorOption[] {
|
||||
const timeGrains: AzureMonitorOption[] = [];
|
||||
|
||||
if (!metricAvailabilities) {
|
||||
return timeGrains;
|
||||
}
|
||||
@ -78,30 +86,22 @@ export default class ResponseParser {
|
||||
metricAvailabilities.forEach((avail) => {
|
||||
if (avail.timeGrain) {
|
||||
timeGrains.push({
|
||||
text: TimeGrainConverter.createTimeGrainFromISO8601Duration(avail.timeGrain),
|
||||
label: TimeGrainConverter.createTimeGrainFromISO8601Duration(avail.timeGrain),
|
||||
value: avail.timeGrain,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return timeGrains;
|
||||
}
|
||||
|
||||
static parseDimensions(metricData: any): Array<{ text: string; value: string }> {
|
||||
const dimensions: Array<{ text: string; value: string }> = [];
|
||||
if (!metricData.dimensions || metricData.dimensions.length === 0) {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
for (let i = 0; i < metricData.dimensions.length; i++) {
|
||||
const text = metricData.dimensions[i].localizedValue;
|
||||
const value = metricData.dimensions[i].value;
|
||||
|
||||
dimensions.push({
|
||||
text: !text ? value : text,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
return dimensions;
|
||||
static parseDimensions(metadataDimensions: AzureMonitorLocalizedValue[]) {
|
||||
return metadataDimensions.map((dimension) => {
|
||||
return {
|
||||
label: dimension.localizedValue || dimension.value,
|
||||
value: dimension.value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static parseSubscriptions(result: any): Array<{ text: string; value: string }> {
|
||||
@ -116,7 +116,7 @@ export default class ResponseParser {
|
||||
for (let i = 0; i < result.data.value.length; i++) {
|
||||
if (!_.find(list, ['value', _.get(result.data.value[i], valueFieldName)])) {
|
||||
list.push({
|
||||
text: `${_.get(result.data.value[i], textFieldName)} - ${_.get(result.data.value[i], valueFieldName)}`,
|
||||
text: `${_.get(result.data.value[i], textFieldName)}`,
|
||||
value: _.get(result.data.value[i], valueFieldName),
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { InlineField } from '@grafana/ui';
|
||||
import React from 'react';
|
||||
import { Props as InlineFieldProps } from '@grafana/ui/src/components/Forms/InlineField';
|
||||
|
||||
const DEFAULT_LABEL_WIDTH = 18;
|
||||
|
||||
export const Field = (props: InlineFieldProps) => {
|
||||
return <InlineField labelWidth={DEFAULT_LABEL_WIDTH} {...props} />;
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { Field } from '../Field';
|
||||
import { findOption } from '../common';
|
||||
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
|
||||
|
||||
interface AggregationFieldProps extends AzureQueryEditorFieldProps {
|
||||
aggregationOptions: AzureMonitorOption[];
|
||||
}
|
||||
|
||||
const AggregationField: React.FC<AggregationFieldProps> = ({
|
||||
query,
|
||||
variableOptionGroup,
|
||||
onQueryChange,
|
||||
aggregationOptions,
|
||||
}) => {
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
if (!change.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
aggregation: change.value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...aggregationOptions, variableOptionGroup], [
|
||||
aggregationOptions,
|
||||
variableOptionGroup,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Field label="Aggregation">
|
||||
<Select
|
||||
inputId="azure-monitor-metrics-aggregation-field"
|
||||
value={findOption(aggregationOptions, query.azureMonitor.aggregation)}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
width={38}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default AggregationField;
|
@ -0,0 +1,89 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Button, Select, Input, HorizontalGroup, VerticalGroup, InlineLabel } from '@grafana/ui';
|
||||
|
||||
import { Field } from '../Field';
|
||||
import { findOption } from '../common';
|
||||
import { AzureMetricDimension, AzureMonitorOption, AzureQueryEditorFieldProps } from '../../types';
|
||||
|
||||
interface DimensionFieldsProps extends AzureQueryEditorFieldProps {
|
||||
dimensionOptions: AzureMonitorOption[];
|
||||
}
|
||||
|
||||
const DimensionFields: React.FC<DimensionFieldsProps> = ({ query, dimensionOptions, onQueryChange }) => {
|
||||
const setDimensionFilters = (newFilters: AzureMetricDimension[]) => {
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
dimensionFilters: newFilters,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const addFilter = useCallback(() => {
|
||||
setDimensionFilters([
|
||||
...query.azureMonitor.dimensionFilters,
|
||||
{
|
||||
dimension: '',
|
||||
operator: 'eq',
|
||||
filter: '',
|
||||
},
|
||||
]);
|
||||
}, [query.azureMonitor.dimensionFilters]);
|
||||
|
||||
const removeFilter = (index: number) => {
|
||||
const newFilters = [...query.azureMonitor.dimensionFilters];
|
||||
newFilters.splice(index, 1);
|
||||
setDimensionFilters(newFilters);
|
||||
};
|
||||
|
||||
const onFieldChange = <Key extends keyof AzureMetricDimension>(
|
||||
filterIndex: number,
|
||||
fieldName: Key,
|
||||
value: AzureMetricDimension[Key]
|
||||
) => {
|
||||
const newFilters = [...query.azureMonitor.dimensionFilters];
|
||||
const newFilter = newFilters[filterIndex];
|
||||
newFilter[fieldName] = value;
|
||||
setDimensionFilters(newFilters);
|
||||
};
|
||||
|
||||
const onFilterInputChange = (index: number, ev: React.FormEvent) => {
|
||||
if (ev.target instanceof HTMLInputElement) {
|
||||
onFieldChange(index, 'filter', ev.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Field label="Dimension">
|
||||
<VerticalGroup spacing="xs">
|
||||
{query.azureMonitor.dimensionFilters.map((filter, index) => (
|
||||
<HorizontalGroup key={index} spacing="xs">
|
||||
<Select
|
||||
placeholder="Field"
|
||||
value={findOption(dimensionOptions, filter.dimension)}
|
||||
options={dimensionOptions}
|
||||
onChange={(v) => onFieldChange(index, 'dimension', v.value ?? '')}
|
||||
width={38}
|
||||
/>
|
||||
<InlineLabel aria-label="equals">==</InlineLabel>
|
||||
<Input placeholder="" value={filter.filter} onChange={(ev) => onFilterInputChange(index, ev)} />
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="md"
|
||||
icon="trash-alt"
|
||||
aria-label="Remove"
|
||||
onClick={() => removeFilter(index)}
|
||||
></Button>
|
||||
</HorizontalGroup>
|
||||
))}
|
||||
|
||||
<Button variant="secondary" size="md" onClick={addFilter}>
|
||||
Add new dimension
|
||||
</Button>
|
||||
</VerticalGroup>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default DimensionFields;
|
@ -0,0 +1,42 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Input } from '@grafana/ui';
|
||||
|
||||
import { Field } from '../Field';
|
||||
import { AzureQueryEditorFieldProps } from '../../types';
|
||||
|
||||
const LegendFormatField: React.FC<AzureQueryEditorFieldProps> = ({ onQueryChange, query }) => {
|
||||
const [value, setValue] = useState<string>(query.azureMonitor.alias ?? '');
|
||||
|
||||
// As calling onQueryChange initiates a the datasource refresh, we only want to call it once
|
||||
// the field loses focus
|
||||
const handleChange = useCallback((ev: React.FormEvent) => {
|
||||
if (ev.target instanceof HTMLInputElement) {
|
||||
setValue(ev.target.value);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
alias: value,
|
||||
},
|
||||
});
|
||||
}, [query, value]);
|
||||
|
||||
return (
|
||||
<Field label="Legend Format">
|
||||
<Input
|
||||
id="azure-monitor-metrics-legend-field"
|
||||
placeholder="Alias patterns"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
width={38}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegendFormatField;
|
@ -0,0 +1,85 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { Field } from '../Field';
|
||||
import { findOption, toOption } from '../common';
|
||||
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
|
||||
|
||||
const MetricName: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
query,
|
||||
datasource,
|
||||
subscriptionId,
|
||||
variableOptionGroup,
|
||||
onQueryChange,
|
||||
}) => {
|
||||
const [metricNames, setMetricNames] = useState<AzureMonitorOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!(
|
||||
subscriptionId &&
|
||||
query.azureMonitor.resourceGroup &&
|
||||
query.azureMonitor.metricDefinition &&
|
||||
query.azureMonitor.resourceName &&
|
||||
query.azureMonitor.metricNamespace
|
||||
)
|
||||
) {
|
||||
metricNames.length > 0 && setMetricNames([]);
|
||||
return;
|
||||
}
|
||||
|
||||
datasource
|
||||
.getMetricNames(
|
||||
subscriptionId,
|
||||
query.azureMonitor.resourceGroup,
|
||||
query.azureMonitor.metricDefinition,
|
||||
query.azureMonitor.resourceName,
|
||||
query.azureMonitor.metricNamespace
|
||||
)
|
||||
.then((results) => setMetricNames(results.map(toOption)))
|
||||
.catch((err) => {
|
||||
// TODO: handle error
|
||||
console.error(err);
|
||||
});
|
||||
}, [
|
||||
subscriptionId,
|
||||
query.azureMonitor.resourceGroup,
|
||||
query.azureMonitor.metricDefinition,
|
||||
query.azureMonitor.resourceName,
|
||||
query.azureMonitor.metricNamespace,
|
||||
]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
if (!change.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
metricName: change.value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...metricNames, variableOptionGroup], [metricNames, variableOptionGroup]);
|
||||
|
||||
return (
|
||||
<Field label="Metric">
|
||||
<Select
|
||||
inputId="azure-monitor-metrics-metric-field"
|
||||
value={findOption(metricNames, query.azureMonitor.metricName)}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
width={38}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricName;
|
@ -0,0 +1,78 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { Field } from '../Field';
|
||||
import { findOption, toOption } from '../common';
|
||||
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
|
||||
|
||||
const MetricNamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
query,
|
||||
datasource,
|
||||
subscriptionId,
|
||||
variableOptionGroup,
|
||||
onQueryChange,
|
||||
}) => {
|
||||
const [metricNamespaces, setMetricNamespaces] = useState<AzureMonitorOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!(subscriptionId && query.azureMonitor.resourceGroup, query.azureMonitor.metricDefinition)) {
|
||||
metricNamespaces.length > 0 && setMetricNamespaces([]);
|
||||
return;
|
||||
}
|
||||
|
||||
datasource
|
||||
.getMetricNamespaces(
|
||||
subscriptionId,
|
||||
query.azureMonitor.resourceGroup,
|
||||
query.azureMonitor.metricDefinition,
|
||||
query.azureMonitor.resourceName
|
||||
)
|
||||
.then((results) => setMetricNamespaces(results.map(toOption)))
|
||||
.catch((err) => {
|
||||
// TODO: handle error
|
||||
console.error(err);
|
||||
});
|
||||
}, [
|
||||
subscriptionId,
|
||||
query.azureMonitor.resourceGroup,
|
||||
query.azureMonitor.metricDefinition,
|
||||
query.azureMonitor.resourceName,
|
||||
]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
if (!change.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
metricNamespace: change.value,
|
||||
|
||||
metricName: 'select',
|
||||
dimensionFilters: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...metricNamespaces, variableOptionGroup], [metricNamespaces, variableOptionGroup]);
|
||||
|
||||
return (
|
||||
<Field label="Metric Namespace">
|
||||
<Select
|
||||
inputId="azure-monitor-metrics-metric-namespace-field"
|
||||
value={findOption(metricNamespaces, query.azureMonitor.metricNamespace)}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
width={38}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricNamespaceField;
|
@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import selectEvent from 'react-select-event';
|
||||
|
||||
import MetricsQueryEditor from './MetricsQueryEditor';
|
||||
|
||||
import mockQuery from '../../__mocks__/query';
|
||||
import createMockDatasource from '../../__mocks__/datasource';
|
||||
|
||||
const variableOptionGroup = {
|
||||
label: 'Template variables',
|
||||
options: [],
|
||||
};
|
||||
|
||||
describe('Azure Monitor QueryEditor', () => {
|
||||
it('should render', async () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
render(
|
||||
<MetricsQueryEditor
|
||||
subscriptionId="123"
|
||||
query={mockQuery}
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should change the subscription ID when selected', async () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
const onChange = jest.fn();
|
||||
mockDatasource.azureMonitorDatasource.getSubscriptions = jest.fn().mockResolvedValueOnce([
|
||||
{
|
||||
value: 'abc-123',
|
||||
text: 'Primary Subscription',
|
||||
},
|
||||
{
|
||||
value: 'abc-456',
|
||||
text: 'Another Subscription',
|
||||
},
|
||||
]);
|
||||
|
||||
render(
|
||||
<MetricsQueryEditor
|
||||
subscriptionId="123"
|
||||
query={mockQuery}
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
const subscriptions = await screen.findByLabelText('Subscription');
|
||||
await selectEvent.select(subscriptions, 'Another Subscription');
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...mockQuery,
|
||||
subscription: 'abc-456',
|
||||
azureMonitor: {
|
||||
...mockQuery.azureMonitor,
|
||||
resourceGroup: 'select',
|
||||
metricDefinition: 'select',
|
||||
resourceName: 'select',
|
||||
metricName: 'select',
|
||||
aggregation: '',
|
||||
timeGrain: '',
|
||||
dimensionFilters: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should change the metric name when selected', async () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
const onChange = jest.fn();
|
||||
mockDatasource.getMetricNames = jest.fn().mockResolvedValueOnce([
|
||||
{
|
||||
value: 'metric-a',
|
||||
text: 'Metric A',
|
||||
},
|
||||
{
|
||||
value: 'metric-b',
|
||||
text: 'Metric B',
|
||||
},
|
||||
]);
|
||||
|
||||
render(
|
||||
<MetricsQueryEditor
|
||||
subscriptionId="123"
|
||||
query={mockQuery}
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument());
|
||||
|
||||
const metrics = await screen.findByLabelText('Metric');
|
||||
await selectEvent.select(metrics, 'Metric B');
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...mockQuery,
|
||||
azureMonitor: {
|
||||
...mockQuery.azureMonitor,
|
||||
metricName: 'metric-b',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,133 @@
|
||||
import React from 'react';
|
||||
|
||||
import Datasource from '../../datasource';
|
||||
import { AzureMonitorQuery, AzureMonitorOption } from '../../types';
|
||||
import { useMetricsMetadata } from '../metrics';
|
||||
import SubscriptionField from '../SubscriptionField';
|
||||
import MetricNamespaceField from './MetricNamespaceField';
|
||||
import NamespaceField from './NamespaceField';
|
||||
import ResourceGroupsField from './ResourceGroupsField';
|
||||
import ResourceNameField from './ResourceNameField';
|
||||
import MetricNameField from './MetricNameField';
|
||||
import AggregationField from './AggregationField';
|
||||
import TimeGrainField from './TimeGrainField';
|
||||
import DimensionFields from './DimensionFields';
|
||||
import TopField from './TopField';
|
||||
import LegendFormatField from './LegendFormatField';
|
||||
import { InlineFieldRow } from '@grafana/ui';
|
||||
|
||||
interface MetricsQueryEditorProps {
|
||||
query: AzureMonitorQuery;
|
||||
datasource: Datasource;
|
||||
subscriptionId: string;
|
||||
onChange: (newQuery: AzureMonitorQuery) => void;
|
||||
variableOptionGroup: { label: string; options: AzureMonitorOption[] };
|
||||
}
|
||||
|
||||
const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
|
||||
query,
|
||||
datasource,
|
||||
subscriptionId,
|
||||
variableOptionGroup,
|
||||
onChange,
|
||||
}) => {
|
||||
const metricsMetadata = useMetricsMetadata(datasource, query, subscriptionId, onChange);
|
||||
|
||||
return (
|
||||
<div data-testid="azure-monitor-metrics-query-editor">
|
||||
<InlineFieldRow>
|
||||
<SubscriptionField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
/>
|
||||
|
||||
<ResourceGroupsField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
/>
|
||||
</InlineFieldRow>
|
||||
|
||||
<InlineFieldRow>
|
||||
<NamespaceField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
/>
|
||||
<ResourceNameField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
/>
|
||||
</InlineFieldRow>
|
||||
|
||||
<InlineFieldRow>
|
||||
<MetricNamespaceField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
/>
|
||||
<MetricNameField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
/>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<AggregationField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
aggregationOptions={metricsMetadata?.aggOptions ?? []}
|
||||
/>
|
||||
<TimeGrainField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
timeGrainOptions={metricsMetadata?.timeGrains ?? []}
|
||||
/>
|
||||
</InlineFieldRow>
|
||||
<DimensionFields
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
dimensionOptions={metricsMetadata?.dimensions ?? []}
|
||||
/>
|
||||
<TopField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
/>
|
||||
<LegendFormatField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetricsQueryEditor;
|
@ -0,0 +1,72 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { Field } from '../Field';
|
||||
import { findOption, toOption } from '../common';
|
||||
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
|
||||
|
||||
const NamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
query,
|
||||
datasource,
|
||||
subscriptionId,
|
||||
variableOptionGroup,
|
||||
onQueryChange,
|
||||
}) => {
|
||||
const [namespaces, setNamespaces] = useState<AzureMonitorOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!(subscriptionId && query.azureMonitor.resourceGroup)) {
|
||||
namespaces.length && setNamespaces([]);
|
||||
return;
|
||||
}
|
||||
|
||||
datasource
|
||||
.getMetricDefinitions(subscriptionId, query.azureMonitor.resourceGroup)
|
||||
.then((results) => setNamespaces(results.map(toOption)))
|
||||
.catch((err) => {
|
||||
// TODO: handle error
|
||||
console.error(err);
|
||||
});
|
||||
}, [subscriptionId, query.azureMonitor.resourceGroup]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
if (!change.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
metricDefinition: change.value,
|
||||
resourceName: 'select',
|
||||
metricNamespace: 'select',
|
||||
metricName: 'select',
|
||||
aggregation: '',
|
||||
timeGrain: '',
|
||||
dimensionFilters: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...namespaces, variableOptionGroup], [namespaces, variableOptionGroup]);
|
||||
|
||||
return (
|
||||
<Field label="Namespace">
|
||||
{/* It's expected that the label reads Namespace but the property is metricDefinition */}
|
||||
<Select
|
||||
inputId="azure-monitor-metrics-namespace-field"
|
||||
value={findOption(namespaces, query.azureMonitor.metricDefinition)}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
width={38}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default NamespaceField;
|
@ -0,0 +1,72 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { Field } from '../Field';
|
||||
import { findOption, toOption } from '../common';
|
||||
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
|
||||
|
||||
const ResourceGroupsField: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
query,
|
||||
datasource,
|
||||
subscriptionId,
|
||||
variableOptionGroup,
|
||||
onQueryChange,
|
||||
}) => {
|
||||
const [resourceGroups, setResourceGroups] = useState<AzureMonitorOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!subscriptionId) {
|
||||
resourceGroups.length > 0 && setResourceGroups([]);
|
||||
return;
|
||||
}
|
||||
|
||||
datasource
|
||||
.getResourceGroups(subscriptionId)
|
||||
.then((results) => setResourceGroups(results.map(toOption)))
|
||||
.catch((err) => {
|
||||
// TODO: handle error
|
||||
console.error(err);
|
||||
});
|
||||
}, [subscriptionId]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
if (!change.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
resourceGroup: change.value,
|
||||
metricDefinition: 'select',
|
||||
resourceName: 'select',
|
||||
metricNamespace: 'select',
|
||||
metricName: 'select',
|
||||
aggregation: '',
|
||||
timeGrain: '',
|
||||
dimensionFilters: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...resourceGroups, variableOptionGroup], [resourceGroups, variableOptionGroup]);
|
||||
|
||||
return (
|
||||
<Field label="Resource Group">
|
||||
<Select
|
||||
inputId="azure-monitor-metrics-resource-group-field"
|
||||
value={findOption(resourceGroups, query.azureMonitor.resourceGroup)}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
width={38}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourceGroupsField;
|
@ -0,0 +1,71 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { Field } from '../Field';
|
||||
import { findOption, toOption } from '../common';
|
||||
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
|
||||
|
||||
const ResourceNameField: React.FC<AzureQueryEditorFieldProps> = ({
|
||||
query,
|
||||
datasource,
|
||||
subscriptionId,
|
||||
variableOptionGroup,
|
||||
onQueryChange,
|
||||
}) => {
|
||||
const [resourceNames, setResourceNames] = useState<AzureMonitorOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!(subscriptionId && query.azureMonitor.resourceGroup && query.azureMonitor.metricDefinition)) {
|
||||
resourceNames.length > 0 && setResourceNames([]);
|
||||
return;
|
||||
}
|
||||
|
||||
datasource
|
||||
.getResourceNames(subscriptionId, query.azureMonitor.resourceGroup, query.azureMonitor.metricDefinition)
|
||||
.then((results) => setResourceNames(results.map(toOption)))
|
||||
.catch((err) => {
|
||||
// TODO: handle error
|
||||
console.error(err);
|
||||
});
|
||||
}, [subscriptionId, query.azureMonitor.resourceGroup, query.azureMonitor.metricDefinition]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
if (!change.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
resourceName: change.value,
|
||||
|
||||
metricNamespace: 'select',
|
||||
metricName: 'select',
|
||||
aggregation: '',
|
||||
timeGrain: '',
|
||||
dimensionFilters: [],
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...resourceNames, variableOptionGroup], [resourceNames, variableOptionGroup]);
|
||||
|
||||
return (
|
||||
<Field label="Resource Name">
|
||||
<Select
|
||||
inputId="azure-monitor-metrics-resource-name-field"
|
||||
value={findOption(resourceNames, query.azureMonitor.resourceName)}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
width={38}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourceNameField;
|
@ -0,0 +1,70 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { Field } from '../Field';
|
||||
import { findOption } from '../common';
|
||||
import TimegrainConverter from '../../time_grain_converter';
|
||||
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
|
||||
|
||||
interface TimeGrainFieldProps extends AzureQueryEditorFieldProps {
|
||||
timeGrainOptions: AzureMonitorOption[];
|
||||
}
|
||||
|
||||
const TimeGrainField: React.FC<TimeGrainFieldProps> = ({
|
||||
query,
|
||||
timeGrainOptions,
|
||||
variableOptionGroup,
|
||||
onQueryChange,
|
||||
}) => {
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
if (!change.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
timeGrain: change.value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[query]
|
||||
);
|
||||
|
||||
const timeGrains = useMemo(() => {
|
||||
const autoInterval = TimegrainConverter.findClosestTimeGrain(
|
||||
'1m',
|
||||
timeGrainOptions.map((o) => TimegrainConverter.createKbnUnitFromISO8601Duration(o.value)) || [
|
||||
'1m',
|
||||
'5m',
|
||||
'15m',
|
||||
'30m',
|
||||
'1h',
|
||||
'6h',
|
||||
'12h',
|
||||
'1d',
|
||||
]
|
||||
);
|
||||
|
||||
const baseTimeGrains = timeGrainOptions.map((v) => (v.value === 'auto' ? { ...v, description: autoInterval } : v));
|
||||
|
||||
return [...baseTimeGrains, variableOptionGroup];
|
||||
}, [timeGrainOptions, variableOptionGroup]);
|
||||
|
||||
return (
|
||||
<Field label="Time Grain">
|
||||
<Select
|
||||
inputId="azure-monitor-metrics-time-grain-field"
|
||||
value={findOption(timeGrainOptions, query.azureMonitor.timeGrain)}
|
||||
onChange={handleChange}
|
||||
options={timeGrains}
|
||||
width={38}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeGrainField;
|
@ -0,0 +1,41 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Input } from '@grafana/ui';
|
||||
|
||||
import { Field } from '../Field';
|
||||
import { AzureQueryEditorFieldProps } from '../../types';
|
||||
|
||||
const TopField: React.FC<AzureQueryEditorFieldProps> = ({ onQueryChange, query }) => {
|
||||
const [value, setValue] = useState<string>(query.azureMonitor.top ?? '');
|
||||
|
||||
// As calling onQueryChange initiates a the datasource refresh, we only want to call it once
|
||||
// the field loses focus
|
||||
const handleChange = useCallback((ev: React.FormEvent) => {
|
||||
if (ev.target instanceof HTMLInputElement) {
|
||||
setValue(ev.target.value);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
top: value,
|
||||
},
|
||||
});
|
||||
}, [query, value]);
|
||||
|
||||
return (
|
||||
<Field label="Top">
|
||||
<Input
|
||||
id="azure-monitor-metrics-top-field"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
width={16}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopField;
|
@ -0,0 +1 @@
|
||||
export { default } from './MetricsQueryEditor';
|
@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import selectEvent from 'react-select-event';
|
||||
|
||||
import QueryEditor from './QueryEditor';
|
||||
|
||||
import mockQuery from '../../__mocks__/query';
|
||||
import createMockDatasource from '../../__mocks__/datasource';
|
||||
import { AzureQueryType } from '../../types';
|
||||
|
||||
const variableOptionGroup = {
|
||||
label: 'Template variables',
|
||||
options: [],
|
||||
};
|
||||
|
||||
describe('Azure Monitor QueryEditor', () => {
|
||||
it('renders the Metrics query editor when the query type is Metrics', async () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
render(
|
||||
<QueryEditor
|
||||
query={mockQuery}
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it("does not render the Metrics query editor when the query type isn't Metrics", async () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
const logsMockQuery = {
|
||||
...mockQuery,
|
||||
queryType: AzureQueryType.LogAnalytics,
|
||||
};
|
||||
render(
|
||||
<QueryEditor
|
||||
query={logsMockQuery}
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => expect(screen.queryByTestId('azure-monitor-metrics-query-editor')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('changes the query type when selected', async () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
const onChange = jest.fn();
|
||||
render(
|
||||
<QueryEditor
|
||||
query={mockQuery}
|
||||
datasource={mockDatasource}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => expect(screen.getByTestId('azure-monitor-query-editor')).toBeInTheDocument());
|
||||
|
||||
const metrics = await screen.findByLabelText('Service');
|
||||
await selectEvent.select(metrics, 'Logs');
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...mockQuery,
|
||||
queryType: AzureQueryType.LogAnalytics,
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import Datasource from '../../datasource';
|
||||
import { AzureMonitorQuery, AzureQueryType, AzureMonitorOption } from '../../types';
|
||||
import MetricsQueryEditor from '../MetricsQueryEditor';
|
||||
import QueryTypeField from './QueryTypeField';
|
||||
|
||||
interface BaseQueryEditorProps {
|
||||
query: AzureMonitorQuery;
|
||||
datasource: Datasource;
|
||||
onChange: (newQuery: AzureMonitorQuery) => void;
|
||||
variableOptionGroup: { label: string; options: AzureMonitorOption[] };
|
||||
}
|
||||
|
||||
const QueryEditor: React.FC<BaseQueryEditorProps> = ({ query, datasource, onChange }) => {
|
||||
const subscriptionId = query.subscription || datasource.azureMonitorDatasource.subscriptionId;
|
||||
const variableOptionGroup = {
|
||||
label: 'Template Variables',
|
||||
options: datasource.getVariables().map((v) => ({ label: v, value: v })),
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="azure-monitor-query-editor">
|
||||
<QueryTypeField query={query} onQueryChange={onChange} />
|
||||
<EditorForQueryType
|
||||
subscriptionId={subscriptionId}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface EditorForQueryTypeProps extends BaseQueryEditorProps {
|
||||
subscriptionId: string;
|
||||
}
|
||||
|
||||
const EditorForQueryType: React.FC<EditorForQueryTypeProps> = ({
|
||||
subscriptionId,
|
||||
query,
|
||||
datasource,
|
||||
variableOptionGroup,
|
||||
onChange,
|
||||
}) => {
|
||||
switch (query.queryType) {
|
||||
case AzureQueryType.AzureMonitor:
|
||||
return (
|
||||
<MetricsQueryEditor
|
||||
subscriptionId={subscriptionId}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default QueryEditor;
|
@ -0,0 +1,45 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { Field } from '../Field';
|
||||
import { AzureMonitorQuery, AzureQueryType } from '../../types';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { findOption } from '../common';
|
||||
|
||||
const QUERY_TYPES = [
|
||||
{ value: AzureQueryType.AzureMonitor, label: 'Metrics' },
|
||||
{ value: AzureQueryType.LogAnalytics, label: 'Logs' },
|
||||
{ value: AzureQueryType.ApplicationInsights, label: 'Application Insights' },
|
||||
{ value: AzureQueryType.InsightsAnalytics, label: 'Insights Analytics' },
|
||||
];
|
||||
|
||||
interface QueryTypeFieldProps {
|
||||
query: AzureMonitorQuery;
|
||||
onQueryChange: (newQuery: AzureMonitorQuery) => void;
|
||||
}
|
||||
|
||||
const QueryTypeField: React.FC<QueryTypeFieldProps> = ({ query, onQueryChange }) => {
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<AzureQueryType>) => {
|
||||
change.value &&
|
||||
onQueryChange({
|
||||
...query,
|
||||
queryType: change.value,
|
||||
});
|
||||
},
|
||||
[query]
|
||||
);
|
||||
|
||||
return (
|
||||
<Field label="Service">
|
||||
<Select
|
||||
inputId="azure-monitor-query-type-field"
|
||||
value={findOption(QUERY_TYPES, query.queryType)}
|
||||
options={QUERY_TYPES}
|
||||
onChange={handleChange}
|
||||
width={38}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryTypeField;
|
@ -0,0 +1 @@
|
||||
export { default } from './QueryEditor';
|
@ -0,0 +1,97 @@
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Select } from '@grafana/ui';
|
||||
|
||||
import { AzureMonitorQuery, AzureQueryType, AzureQueryEditorFieldProps, AzureMonitorOption } from '../types';
|
||||
import { findOption } from './common';
|
||||
import { Field } from './Field';
|
||||
|
||||
interface SubscriptionFieldProps extends AzureQueryEditorFieldProps {
|
||||
onQueryChange: (newQuery: AzureMonitorQuery) => void;
|
||||
}
|
||||
|
||||
const SubscriptionField: React.FC<SubscriptionFieldProps> = ({
|
||||
datasource,
|
||||
query,
|
||||
variableOptionGroup,
|
||||
onQueryChange,
|
||||
}) => {
|
||||
const [subscriptions, setSubscriptions] = useState<AzureMonitorOption[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!datasource.azureMonitorDatasource.isConfigured()) {
|
||||
return;
|
||||
}
|
||||
|
||||
datasource.azureMonitorDatasource.getSubscriptions().then((results) => {
|
||||
const newSubscriptions = results.map((v) => ({ label: v.text, value: v.value, description: v.value }));
|
||||
setSubscriptions(newSubscriptions);
|
||||
|
||||
// Set a default subscription ID, if we can
|
||||
let newSubscription = query.subscription;
|
||||
|
||||
if (!newSubscription && query.queryType === AzureQueryType.AzureMonitor) {
|
||||
newSubscription = datasource.azureMonitorDatasource.subscriptionId;
|
||||
} else if (!query.subscription && query.queryType === AzureQueryType.LogAnalytics) {
|
||||
newSubscription =
|
||||
datasource.azureLogAnalyticsDatasource.logAnalyticsSubscriptionId ||
|
||||
datasource.azureLogAnalyticsDatasource.subscriptionId;
|
||||
}
|
||||
|
||||
if (!newSubscription && newSubscriptions.length > 0) {
|
||||
newSubscription = newSubscriptions[0].value;
|
||||
}
|
||||
|
||||
newSubscription !== query.subscription &&
|
||||
onQueryChange({
|
||||
...query,
|
||||
subscription: newSubscription,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(change: SelectableValue<string>) => {
|
||||
if (!change.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newQuery: AzureMonitorQuery = {
|
||||
...query,
|
||||
subscription: change.value,
|
||||
};
|
||||
|
||||
if (query.queryType === AzureQueryType.AzureMonitor) {
|
||||
newQuery.azureMonitor = {
|
||||
...newQuery.azureMonitor,
|
||||
resourceGroup: 'select',
|
||||
metricDefinition: 'select',
|
||||
resourceName: 'select',
|
||||
metricName: 'select',
|
||||
aggregation: '',
|
||||
timeGrain: '',
|
||||
dimensionFilters: [],
|
||||
};
|
||||
}
|
||||
|
||||
onQueryChange(newQuery);
|
||||
},
|
||||
[query, onQueryChange]
|
||||
);
|
||||
|
||||
const options = useMemo(() => [...subscriptions, variableOptionGroup], [subscriptions, variableOptionGroup]);
|
||||
|
||||
return (
|
||||
<Field label="Subscription">
|
||||
<Select
|
||||
value={findOption(subscriptions, query.subscription)}
|
||||
inputId="azure-monitor-subscriptions-field"
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
width={38}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubscriptionField;
|
@ -0,0 +1,19 @@
|
||||
import { rangeUtil } from '@grafana/data';
|
||||
import TimegrainConverter from '../time_grain_converter';
|
||||
import { AzureMonitorOption } from '../types';
|
||||
|
||||
// Defaults to returning a fallback option so the UI still shows the value while the API is loading
|
||||
export const findOption = (options: AzureMonitorOption[], value: string) =>
|
||||
options.find((v) => v.value === value) ?? { value, label: value };
|
||||
|
||||
export const toOption = (v: { text: string; value: string }) => ({ value: v.value, label: v.text });
|
||||
|
||||
export function convertTimeGrainsToMs<T extends { value: string }>(timeGrains: T[]) {
|
||||
const allowedTimeGrainsMs: number[] = [];
|
||||
timeGrains.forEach((tg: any) => {
|
||||
if (tg.value !== 'auto') {
|
||||
allowedTimeGrainsMs.push(rangeUtil.intervalToMs(TimegrainConverter.createKbnUnitFromISO8601Duration(tg.value)));
|
||||
}
|
||||
});
|
||||
return allowedTimeGrainsMs;
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import Datasource from '../datasource';
|
||||
import { AzureMonitorQuery } from '../types';
|
||||
import { convertTimeGrainsToMs } from './common';
|
||||
|
||||
export interface MetricMetadata {
|
||||
aggOptions: Array<{ label: string; value: string }>;
|
||||
timeGrains: Array<{ label: string; value: string }>;
|
||||
dimensions: Array<{ label: string; value: string }>;
|
||||
}
|
||||
|
||||
export function useMetricsMetadata(
|
||||
datasource: Datasource,
|
||||
query: AzureMonitorQuery,
|
||||
subscriptionId: string,
|
||||
onQueryChange: (newQuery: AzureMonitorQuery) => void
|
||||
) {
|
||||
const [metricMetadata, setMetricMetadata] = useState<MetricMetadata>({
|
||||
aggOptions: [],
|
||||
timeGrains: [],
|
||||
dimensions: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!(
|
||||
subscriptionId &&
|
||||
query.azureMonitor.resourceGroup &&
|
||||
query.azureMonitor.metricDefinition &&
|
||||
query.azureMonitor.resourceName &&
|
||||
query.azureMonitor.metricNamespace &&
|
||||
query.azureMonitor.metricName
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
datasource
|
||||
.getMetricMetadata(
|
||||
subscriptionId,
|
||||
query.azureMonitor.resourceGroup,
|
||||
query.azureMonitor.metricDefinition,
|
||||
query.azureMonitor.resourceName,
|
||||
query.azureMonitor.metricNamespace,
|
||||
query.azureMonitor.metricName
|
||||
)
|
||||
.then((metadata) => {
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureMonitor: {
|
||||
...query.azureMonitor,
|
||||
aggregation: metadata.primaryAggType,
|
||||
timeGrain: 'auto',
|
||||
allowedTimeGrainsMs: convertTimeGrainsToMs(metadata.supportedTimeGrains),
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Move the aggregationTypes and timeGrain defaults into `getMetricMetadata`
|
||||
const aggregations = (metadata.supportedAggTypes || [metadata.primaryAggType]).map((v) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
}));
|
||||
|
||||
setMetricMetadata({
|
||||
aggOptions: aggregations,
|
||||
timeGrains: metadata.supportedTimeGrains,
|
||||
dimensions: metadata.dimensions,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO: handle error
|
||||
console.error(err);
|
||||
});
|
||||
}, [
|
||||
subscriptionId,
|
||||
query.azureMonitor.resourceGroup,
|
||||
query.azureMonitor.metricDefinition,
|
||||
query.azureMonitor.resourceName,
|
||||
query.azureMonitor.metricNamespace,
|
||||
query.azureMonitor.metricName,
|
||||
]);
|
||||
|
||||
return metricMetadata;
|
||||
}
|
@ -13,7 +13,7 @@ import {
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
import { forkJoin, Observable, of } from 'rxjs';
|
||||
import { DataSourceWithBackend } from '@grafana/runtime';
|
||||
import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
import InsightsAnalyticsDatasource from './insights_analytics/insights_analytics_datasource';
|
||||
import { migrateMetricsDimensionFilters } from './query_ctrl';
|
||||
import { map } from 'rxjs/operators';
|
||||
@ -27,7 +27,10 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
|
||||
pseudoDatasource: Record<AzureQueryType, DataSourceWithBackend>;
|
||||
optionsKey: Record<AzureQueryType, string>;
|
||||
|
||||
constructor(instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>) {
|
||||
constructor(
|
||||
instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>,
|
||||
private readonly templateSrv: TemplateSrv = getTemplateSrv()
|
||||
) {
|
||||
super(instanceSettings);
|
||||
this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings);
|
||||
this.appInsightsDatasource = new AppInsightsDatasource(instanceSettings);
|
||||
@ -190,15 +193,22 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
|
||||
|
||||
/* Azure Monitor REST API methods */
|
||||
getResourceGroups(subscriptionId: string) {
|
||||
return this.azureMonitorDatasource.getResourceGroups(subscriptionId);
|
||||
return this.azureMonitorDatasource.getResourceGroups(this.replaceTemplateVariable(subscriptionId));
|
||||
}
|
||||
|
||||
getMetricDefinitions(subscriptionId: string, resourceGroup: string) {
|
||||
return this.azureMonitorDatasource.getMetricDefinitions(subscriptionId, resourceGroup);
|
||||
return this.azureMonitorDatasource.getMetricDefinitions(
|
||||
this.replaceTemplateVariable(subscriptionId),
|
||||
this.replaceTemplateVariable(resourceGroup)
|
||||
);
|
||||
}
|
||||
|
||||
getResourceNames(subscriptionId: string, resourceGroup: string, metricDefinition: string) {
|
||||
return this.azureMonitorDatasource.getResourceNames(subscriptionId, resourceGroup, metricDefinition);
|
||||
return this.azureMonitorDatasource.getResourceNames(
|
||||
this.replaceTemplateVariable(subscriptionId),
|
||||
this.replaceTemplateVariable(resourceGroup),
|
||||
this.replaceTemplateVariable(metricDefinition)
|
||||
);
|
||||
}
|
||||
|
||||
getMetricNames(
|
||||
@ -209,20 +219,20 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
|
||||
metricNamespace: string
|
||||
) {
|
||||
return this.azureMonitorDatasource.getMetricNames(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
metricDefinition,
|
||||
resourceName,
|
||||
metricNamespace
|
||||
this.replaceTemplateVariable(subscriptionId),
|
||||
this.replaceTemplateVariable(resourceGroup),
|
||||
this.replaceTemplateVariable(metricDefinition),
|
||||
this.replaceTemplateVariable(resourceName),
|
||||
this.replaceTemplateVariable(metricNamespace)
|
||||
);
|
||||
}
|
||||
|
||||
getMetricNamespaces(subscriptionId: string, resourceGroup: string, metricDefinition: string, resourceName: string) {
|
||||
return this.azureMonitorDatasource.getMetricNamespaces(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
metricDefinition,
|
||||
resourceName
|
||||
this.replaceTemplateVariable(subscriptionId),
|
||||
this.replaceTemplateVariable(resourceGroup),
|
||||
this.replaceTemplateVariable(metricDefinition),
|
||||
this.replaceTemplateVariable(resourceName)
|
||||
);
|
||||
}
|
||||
|
||||
@ -235,12 +245,12 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
|
||||
metricName: string
|
||||
) {
|
||||
return this.azureMonitorDatasource.getMetricMetadata(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
metricDefinition,
|
||||
resourceName,
|
||||
metricNamespace,
|
||||
metricName
|
||||
this.replaceTemplateVariable(subscriptionId),
|
||||
this.replaceTemplateVariable(resourceGroup),
|
||||
this.replaceTemplateVariable(metricDefinition),
|
||||
this.replaceTemplateVariable(resourceName),
|
||||
this.replaceTemplateVariable(metricNamespace),
|
||||
this.replaceTemplateVariable(metricName)
|
||||
);
|
||||
}
|
||||
|
||||
@ -271,4 +281,12 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
|
||||
(query) => this.pseudoDatasource[query.queryType].applyTemplateVariables(query, scopedVars) as AzureMonitorQuery
|
||||
);
|
||||
}
|
||||
|
||||
replaceTemplateVariable(variable: string) {
|
||||
return this.templateSrv.replace(variable);
|
||||
}
|
||||
|
||||
getVariables() {
|
||||
return this.templateSrv.getVariables().map((v) => `$${v.name}`);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
<query-editor-row query-ctrl="ctrl" can-collapse="false">
|
||||
<query-editor-row
|
||||
query-ctrl="ctrl"
|
||||
can-collapse="false"
|
||||
ng-if="!ctrl.reactQueryEditors.includes(ctrl.target.queryType)"
|
||||
>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label query-keyword width-9">Service</label>
|
||||
@ -519,3 +523,9 @@
|
||||
</p>
|
||||
</div>
|
||||
</query-editor-row>
|
||||
|
||||
<!-- Partial migration to React -->
|
||||
<div ng-if="ctrl.reactQueryEditors.includes(ctrl.target.queryType)">
|
||||
<azure-monitor-query-editor query="ctrl.target" datasource="ctrl.datasource" on-change="ctrl.handleNewQuery">
|
||||
</azure-monitor-query-editor>
|
||||
</div>
|
||||
|
@ -7,7 +7,9 @@ import './editor/editor_component';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { auto, IPromise } from 'angular';
|
||||
import { DataFrame, PanelEvents, rangeUtil } from '@grafana/data';
|
||||
import { AzureQueryType, AzureMetricQuery } from './types';
|
||||
import { AzureQueryType, AzureMetricQuery, AzureMonitorQuery } from './types';
|
||||
import { convertTimeGrainsToMs } from './components/common';
|
||||
import Datasource from './datasource';
|
||||
|
||||
export interface ResultFormat {
|
||||
text: string;
|
||||
@ -28,6 +30,11 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
||||
{ id: AzureQueryType.InsightsAnalytics, label: 'Insights Analytics' },
|
||||
];
|
||||
|
||||
// Query types that have been migrated to React
|
||||
reactQueryEditors = [AzureQueryType.AzureMonitor];
|
||||
|
||||
// target: AzureMonitorQuery;
|
||||
|
||||
target: {
|
||||
// should be: AzureMonitorQuery
|
||||
refId: string;
|
||||
@ -217,7 +224,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
||||
oldAzureTimeGrains.length > 0 &&
|
||||
(!this.target.azureMonitor.allowedTimeGrainsMs || this.target.azureMonitor.allowedTimeGrainsMs.length === 0)
|
||||
) {
|
||||
this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(oldAzureTimeGrains);
|
||||
this.target.azureMonitor.allowedTimeGrainsMs = convertTimeGrainsToMs(oldAzureTimeGrains);
|
||||
}
|
||||
|
||||
if (
|
||||
@ -225,7 +232,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
||||
this.target.appInsights.timeGrains.length > 0 &&
|
||||
(!this.target.appInsights.allowedTimeGrainsMs || this.target.appInsights.allowedTimeGrainsMs.length === 0)
|
||||
) {
|
||||
this.target.appInsights.allowedTimeGrainsMs = this.convertTimeGrainsToMs(this.target.appInsights.timeGrains);
|
||||
this.target.appInsights.allowedTimeGrainsMs = convertTimeGrainsToMs(this.target.appInsights.timeGrains);
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,9 +286,9 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
replace(variable: string) {
|
||||
replace = (variable: string) => {
|
||||
return this.templateSrv.replace(variable, this.panelCtrl.panel.scopedVars);
|
||||
}
|
||||
};
|
||||
|
||||
onQueryTypeChange() {
|
||||
if (this.target.queryType === 'Azure Log Analytics') {
|
||||
@ -294,7 +301,18 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.datasource.azureMonitorDatasource.getSubscriptions().then((subs: any) => {
|
||||
// assert the type
|
||||
if (!(this.datasource instanceof Datasource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.datasource.azureMonitorDatasource.getSubscriptions().then((subscriptions) => {
|
||||
// We changed the format in the datasource for the new react stuff, so here we change it back
|
||||
const subs = subscriptions.map((v) => ({
|
||||
text: `${v.text} - ${v.value}`,
|
||||
value: v.value,
|
||||
}));
|
||||
|
||||
this.subscriptions = subs;
|
||||
if (!this.target.subscription && this.target.queryType === 'Azure Monitor') {
|
||||
this.target.subscription = this.datasource.azureMonitorDatasource.subscriptionId;
|
||||
@ -475,7 +493,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
||||
.then((metadata: any) => {
|
||||
this.target.azureMonitor.aggregation = metadata.primaryAggType;
|
||||
this.target.azureMonitor.timeGrain = 'auto';
|
||||
this.target.azureMonitor.allowedTimeGrainsMs = this.convertTimeGrainsToMs(metadata.supportedTimeGrains || []);
|
||||
this.target.azureMonitor.allowedTimeGrainsMs = convertTimeGrainsToMs(metadata.supportedTimeGrains || []);
|
||||
|
||||
// HACK: this saves the last metadata values in the panel json ¯\_(ツ)_/¯
|
||||
const hackState = this.target.azureMonitor as any;
|
||||
@ -492,6 +510,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
||||
.catch(this.handleQueryCtrlError.bind(this));
|
||||
}
|
||||
|
||||
// This is reimplement
|
||||
convertTimeGrainsToMs(timeGrains: Array<{ text: string; value: string }>) {
|
||||
const allowedTimeGrainsMs: number[] = [];
|
||||
timeGrains.forEach((tg: any) => {
|
||||
@ -683,6 +702,14 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a full new query object from React and updates it into the Angular controller
|
||||
*/
|
||||
handleNewQuery = (newQuery: AzureMonitorQuery) => {
|
||||
Object.assign(this.target, newQuery);
|
||||
this.refresh();
|
||||
};
|
||||
}
|
||||
|
||||
// Modifies the actual query object
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { DataQuery, DataSourceJsonData, DataSourceSettings, TableData } from '@grafana/data';
|
||||
import Datasource from './datasource';
|
||||
|
||||
export type AzureDataSourceSettings = DataSourceSettings<AzureDataSourceJsonData, AzureDataSourceSecureJsonData>;
|
||||
|
||||
@ -91,6 +92,30 @@ export interface InsightsAnalyticsQuery {
|
||||
|
||||
// Azure Monitor API Types
|
||||
|
||||
export interface AzureMonitorMetricsMetadataResponse {
|
||||
value: AzureMonitorMetricMetadataItem[];
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricMetadataItem {
|
||||
id: string;
|
||||
resourceId: string;
|
||||
primaryAggregationType: string;
|
||||
supportedAggregationTypes: string[];
|
||||
name: AzureMonitorLocalizedValue;
|
||||
dimensions?: AzureMonitorLocalizedValue[];
|
||||
metricAvailabilities?: AzureMonitorMetricAvailabilityMetadata[];
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricAvailabilityMetadata {
|
||||
timeGrain: string;
|
||||
retention: string;
|
||||
}
|
||||
|
||||
export interface AzureMonitorLocalizedValue {
|
||||
value: string;
|
||||
localizedValue: string;
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricDefinitionsResponse {
|
||||
data: {
|
||||
value: Array<{ name: string; type: string; location?: string }>;
|
||||
@ -153,3 +178,17 @@ export interface AzureLogsTableColumn {
|
||||
text: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface AzureMonitorOption<T = string> {
|
||||
label: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export interface AzureQueryEditorFieldProps {
|
||||
query: AzureMonitorQuery;
|
||||
datasource: Datasource;
|
||||
subscriptionId: string;
|
||||
variableOptionGroup: { label: string; options: AzureMonitorOption[] };
|
||||
|
||||
onQueryChange: (newQuery: AzureMonitorQuery) => void;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user