AzureMonitor: Refactor query state and fetching data for Metrics (#37067)

* Split query modifications into seperate file

* seperate setQueryValue for Logs

* wip for centralising api requests

* more wip

* moved data hooks out into seperate module, added post-request cleanup

* metric metadata

* ,

* more cleanup

* update tests, fix hasOption to work with grouped variables

* update ARG subscriptiopns field

* last select stuff

* remove todo comment

* pr feedback

* more pr feedback :) :)

* updated comment
This commit is contained in:
Josh Hunt 2021-08-03 10:04:04 +01:00 committed by GitHub
parent f10c5020f1
commit e530d66275
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 773 additions and 460 deletions

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState, useRef } from 'react';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
import Datasource from '../../datasource';
import { InlineFieldRow } from '@grafana/ui';
@ -14,6 +14,7 @@ interface LogsQueryEditorProps {
setError: (source: string, error: AzureMonitorErrorish | undefined) => void;
}
const ERROR_SOURCE = 'arg-subscriptions';
const ArgQueryEditor: React.FC<LogsQueryEditorProps> = ({
query,
datasource,
@ -22,11 +23,38 @@ const ArgQueryEditor: React.FC<LogsQueryEditorProps> = ({
onChange,
setError,
}) => {
const fetchedRef = useRef(false);
const [subscriptions, setSubscriptions] = useState<AzureMonitorOption[]>([]);
useEffect(() => {
if (fetchedRef.current) {
return;
}
fetchedRef.current = true;
datasource.azureMonitorDatasource
.getSubscriptions()
.then((results) => {
const fetchedSubscriptions = results.map((v) => ({ label: v.text, value: v.value, description: v.value }));
setSubscriptions(fetchedSubscriptions);
setError(ERROR_SOURCE, undefined);
if (!query.subscriptions?.length && fetchedSubscriptions?.length) {
onChange({
...query,
subscriptions: [query.subscription ?? fetchedSubscriptions[0].value],
});
}
})
.catch((err) => setError(ERROR_SOURCE, err));
}, [datasource, onChange, query, setError]);
return (
<div data-testid="azure-monitor-logs-query-editor">
<InlineFieldRow>
<SubscriptionField
multiSelect
subscriptions={subscriptions}
query={query}
datasource={datasource}
subscriptionId={subscriptionId}

View File

@ -1,7 +1,6 @@
import { Alert, CodeEditor, Select } from '@grafana/ui';
import React from 'react';
import { AzureMonitorOption, AzureMonitorQuery, AzureResultFormat } from '../../types';
import { findOption } from '../../utils/common';
import { Field } from '../Field';
import { Space } from '../Space';
@ -29,7 +28,7 @@ const InsightsAnalyticsEditor: React.FC<InsightsAnalyticsEditorProps> = ({ query
<Field label="Format as">
<Select
inputId="azure-monitor-logs-workspaces-field"
value={findOption(FORMAT_OPTIONS, query.insightsAnalytics?.resultFormat)}
value={query.insightsAnalytics?.resultFormat}
disabled={true}
options={FORMAT_OPTIONS}
onChange={() => {}}

View File

@ -2,8 +2,8 @@ import React, { useCallback, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { AzureMonitorOption, AzureQueryEditorFieldProps, AzureResultFormat } from '../../types';
import { findOption } from '../../utils/common';
import { Field } from '../Field';
import { setFormatAs } from './setQueryValue';
const FORMAT_OPTIONS: Array<AzureMonitorOption<AzureResultFormat>> = [
{ label: 'Time series', value: 'time_series' },
@ -20,13 +20,8 @@ const FormatAsField: React.FC<AzureQueryEditorFieldProps> = ({ query, variableOp
return;
}
onQueryChange({
...query,
azureLogAnalytics: {
...query.azureLogAnalytics,
resultFormat: value,
},
});
const newQuery = setFormatAs(query, value);
onQueryChange(newQuery);
},
[onQueryChange, query]
);
@ -35,7 +30,7 @@ const FormatAsField: React.FC<AzureQueryEditorFieldProps> = ({ query, variableOp
<Field label="Format as">
<Select
inputId="azure-monitor-logs-workspaces-field"
value={findOption(FORMAT_OPTIONS, query.azureLogAnalytics?.resultFormat)}
value={query.azureLogAnalytics?.resultFormat}
onChange={handleChange}
options={options}
width={38}

View File

@ -2,6 +2,7 @@ import { CodeEditor, Monaco, MonacoEditor } from '@grafana/ui';
import { Deferred } from 'app/core/utils/deferred';
import React, { useCallback, useEffect, useRef } from 'react';
import { AzureQueryEditorFieldProps } from '../../types';
import { setKustoQuery } from './setQueryValue';
interface MonacoPromise {
editor: MonacoEditor;
@ -44,15 +45,15 @@ const QueryField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource, o
Promise.all(promises).then(([schema, { monaco, editor }]) => {
const languages = (monaco.languages as unknown) as MonacoLanguages;
languages.kusto.getKustoWorker().then((kusto) => {
const model = editor.getModel();
if (!model) {
return;
}
kusto(model.uri).then((worker) => {
worker.setSchema(schema, 'https://help.kusto.windows.net', 'Samples');
languages.kusto
.getKustoWorker()
.then((kusto) => {
const model = editor.getModel();
return model && kusto(model.uri);
})
.then((worker) => {
worker?.setSchema(schema, 'https://help.kusto.windows.net', 'Samples');
});
});
});
}, [datasource.azureLogAnalyticsDatasource, query.azureLogAnalytics?.resource]);
@ -62,13 +63,7 @@ const QueryField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource, o
const onChange = useCallback(
(newQuery: string) => {
onQueryChange({
...query,
azureLogAnalytics: {
...query.azureLogAnalytics,
query: newQuery,
},
});
onQueryChange(setKustoQuery(query, newQuery));
},
[onQueryChange, query]
);

View File

@ -8,6 +8,7 @@ import { Field } from '../Field';
import ResourcePicker from '../ResourcePicker';
import { parseResourceURI } from '../ResourcePicker/utils';
import { Space } from '../Space';
import { setResource } from './setQueryValue';
function parseResourceDetails(resourceURI: string) {
const parsed = parseResourceURI(resourceURI);
@ -39,13 +40,7 @@ const ResourceField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource
const handleApply = useCallback(
(resourceURI: string | undefined) => {
onQueryChange({
...query,
azureLogAnalytics: {
...query.azureLogAnalytics,
resource: resourceURI,
},
});
onQueryChange(setResource(query, resourceURI));
closePicker();
},
[closePicker, onQueryChange, query]

View File

@ -0,0 +1,31 @@
import { AzureMonitorQuery } from '../../types';
export function setKustoQuery(query: AzureMonitorQuery, kustoQuery: string): AzureMonitorQuery {
return {
...query,
azureLogAnalytics: {
...query.azureLogAnalytics,
query: kustoQuery,
},
};
}
export function setFormatAs(query: AzureMonitorQuery, formatAs: string): AzureMonitorQuery {
return {
...query,
azureLogAnalytics: {
...query.azureLogAnalytics,
resultFormat: formatAs,
},
};
}
export function setResource(query: AzureMonitorQuery, resourceURI: string | undefined): AzureMonitorQuery {
return {
...query,
azureLogAnalytics: {
...query.azureLogAnalytics,
resource: resourceURI,
},
};
}

View File

@ -3,8 +3,8 @@ import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { Field } from '../Field';
import { findOption } from '../../utils/common';
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
import { setAggregation } from './setQueryValue';
interface AggregationFieldProps extends AzureQueryEditorFieldProps {
aggregationOptions: AzureMonitorOption[];
@ -24,13 +24,8 @@ const AggregationField: React.FC<AggregationFieldProps> = ({
return;
}
onQueryChange({
...query,
azureMonitor: {
...query.azureMonitor,
aggregation: change.value,
},
});
const newQuery = setAggregation(query, change.value);
onQueryChange(newQuery);
},
[onQueryChange, query]
);
@ -44,7 +39,7 @@ const AggregationField: React.FC<AggregationFieldProps> = ({
<Field label="Aggregation">
<Select
inputId="azure-monitor-metrics-aggregation-field"
value={findOption(aggregationOptions, query.azureMonitor?.aggregation)}
value={query.azureMonitor?.aggregation}
onChange={handleChange}
options={options}
width={38}

View File

@ -1,9 +1,9 @@
import React, { useCallback, useMemo } from 'react';
import React, { useMemo } from 'react';
import { Button, Select, Input, HorizontalGroup, VerticalGroup, InlineLabel } from '@grafana/ui';
import { Field } from '../Field';
import { findOption } from '../../utils/common';
import { AzureMetricDimension, AzureMonitorOption, AzureQueryEditorFieldProps } from '../../types';
import { appendDimensionFilter, removeDimensionFilter, setDimensionFilterValue } from './setQueryValue';
interface DimensionFieldsProps extends AzureQueryEditorFieldProps {
dimensionOptions: AzureMonitorOption[];
@ -14,34 +14,12 @@ const DimensionFields: React.FC<DimensionFieldsProps> = ({ query, dimensionOptio
query.azureMonitor?.dimensionFilters,
]);
const setDimensionFilters = useCallback(
(newFilters: AzureMetricDimension[]) => {
onQueryChange({
...query,
azureMonitor: {
...query.azureMonitor,
dimensionFilters: newFilters,
},
});
},
[onQueryChange, query]
);
const addFilter = useCallback(() => {
setDimensionFilters([
...dimensionFilters,
{
dimension: '',
operator: 'eq',
filter: '',
},
]);
}, [dimensionFilters, setDimensionFilters]);
const addFilter = () => {
onQueryChange(appendDimensionFilter(query));
};
const removeFilter = (index: number) => {
const newFilters = [...dimensionFilters];
newFilters.splice(index, 1);
setDimensionFilters(newFilters);
onQueryChange(removeDimensionFilter(query, index));
};
const onFieldChange = <Key extends keyof AzureMetricDimension>(
@ -49,10 +27,7 @@ const DimensionFields: React.FC<DimensionFieldsProps> = ({ query, dimensionOptio
fieldName: Key,
value: AzureMetricDimension[Key]
) => {
const newFilters = [...dimensionFilters];
const newFilter = newFilters[filterIndex];
newFilter[fieldName] = value;
setDimensionFilters(newFilters);
onQueryChange(setDimensionFilterValue(query, filterIndex, fieldName, value));
};
const onFilterInputChange = (index: number, ev: React.FormEvent) => {
@ -68,7 +43,7 @@ const DimensionFields: React.FC<DimensionFieldsProps> = ({ query, dimensionOptio
<HorizontalGroup key={index} spacing="xs">
<Select
placeholder="Field"
value={findOption(dimensionOptions, filter.dimension)}
value={filter.dimension}
options={dimensionOptions}
onChange={(v) => onFieldChange(index, 'dimension', v.value ?? '')}
width={38}

View File

@ -3,6 +3,7 @@ import { Input } from '@grafana/ui';
import { Field } from '../Field';
import { AzureQueryEditorFieldProps } from '../../types';
import { setLegendAlias } from './setQueryValue';
const LegendFormatField: React.FC<AzureQueryEditorFieldProps> = ({ onQueryChange, query }) => {
const [value, setValue] = useState<string>(query.azureMonitor?.alias ?? '');
@ -16,13 +17,8 @@ const LegendFormatField: React.FC<AzureQueryEditorFieldProps> = ({ onQueryChange
}, []);
const handleBlur = useCallback(() => {
onQueryChange({
...query,
azureMonitor: {
...query.azureMonitor,
alias: value,
},
});
const newQuery = setLegendAlias(query, value);
onQueryChange(newQuery);
}, [onQueryChange, query, value]);
return (

View File

@ -1,56 +1,24 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { Field } from '../Field';
import { findOption, toOption } from '../../utils/common';
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
import { setMetricName } from './setQueryValue';
const ERROR_SOURCE = 'metrics-metricname';
const MetricName: React.FC<AzureQueryEditorFieldProps> = ({
query,
datasource,
subscriptionId,
variableOptionGroup,
onQueryChange,
setError,
}) => {
const [metricNames, setMetricNames] = useState<AzureMonitorOption[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const { resourceGroup, metricDefinition, resourceName, metricNamespace } = query.azureMonitor ?? {};
if (!(subscriptionId && resourceGroup && metricDefinition && resourceName && metricNamespace)) {
metricNames.length > 0 && setMetricNames([]);
return;
}
setIsLoading(true);
datasource
.getMetricNames(subscriptionId, resourceGroup, metricDefinition, resourceName, metricNamespace)
.then((results) => {
setMetricNames(results.map(toOption));
setIsLoading(false);
})
.catch((err) => {
setError(ERROR_SOURCE, err);
setIsLoading(false);
});
}, [datasource, metricNames.length, query.azureMonitor, setError, subscriptionId]);
interface MetricNameProps extends AzureQueryEditorFieldProps {
metricNames: AzureMonitorOption[];
}
const MetricNameField: React.FC<MetricNameProps> = ({ metricNames, query, variableOptionGroup, onQueryChange }) => {
const handleChange = useCallback(
(change: SelectableValue<string>) => {
if (!change.value) {
return;
}
onQueryChange({
...query,
azureMonitor: {
...query.azureMonitor,
metricName: change.value,
},
});
const newQuery = setMetricName(query, change.value);
onQueryChange(newQuery);
},
[onQueryChange, query]
);
@ -61,14 +29,13 @@ const MetricName: React.FC<AzureQueryEditorFieldProps> = ({
<Field label="Metric">
<Select
inputId="azure-monitor-metrics-metric-field"
value={findOption(metricNames, query.azureMonitor?.metricName)}
value={query.azureMonitor?.metricName ?? null}
onChange={handleChange}
options={options}
width={38}
isLoading={isLoading}
/>
</Field>
);
};
export default MetricName;
export default MetricNameField;

View File

@ -1,68 +1,29 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { Field } from '../Field';
import { findOption, toOption } from '../../utils/common';
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
import { setMetricNamespace } from './setQueryValue';
const ERROR_SOURCE = 'metrics-metricnamespace';
const MetricNamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
interface MetricNamespaceFieldProps extends AzureQueryEditorFieldProps {
metricNamespaces: AzureMonitorOption[];
}
const MetricNamespaceField: React.FC<MetricNamespaceFieldProps> = ({
metricNamespaces,
query,
datasource,
subscriptionId,
variableOptionGroup,
onQueryChange,
setError,
}) => {
const [metricNamespaces, setMetricNamespaces] = useState<AzureMonitorOption[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const { resourceGroup, metricDefinition, resourceName } = query.azureMonitor ?? {};
if (!(subscriptionId && resourceGroup && metricDefinition && resourceName)) {
metricNamespaces.length > 0 && setMetricNamespaces([]);
return;
}
setIsLoading(true);
datasource
.getMetricNamespaces(subscriptionId, resourceGroup, metricDefinition, resourceName)
.then((results) => {
if (results.length === 1) {
onQueryChange({
...query,
azureMonitor: {
...query.azureMonitor,
metricNamespace: results[0].value,
},
});
}
setMetricNamespaces(results.map(toOption));
setIsLoading(false);
})
.catch((err) => {
setError(ERROR_SOURCE, err);
setIsLoading(false);
});
}, [datasource, metricNamespaces.length, onQueryChange, query, setError, subscriptionId]);
const handleChange = useCallback(
(change: SelectableValue<string>) => {
if (!change.value) {
return;
}
onQueryChange({
...query,
azureMonitor: {
...query.azureMonitor,
metricNamespace: change.value,
metricName: undefined,
dimensionFilters: [],
},
});
const newQuery = setMetricNamespace(query, change.value);
onQueryChange(newQuery);
},
[onQueryChange, query]
);
@ -73,11 +34,10 @@ const MetricNamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
<Field label="Metric namespace">
<Select
inputId="azure-monitor-metrics-metric-namespace-field"
value={findOption(metricNamespaces, query.azureMonitor?.metricNamespace)}
value={query.azureMonitor?.metricNamespace}
onChange={handleChange}
options={options}
width={38}
isLoading={isLoading}
/>
</Field>
);

View File

@ -2,7 +2,6 @@ import React from 'react';
import Datasource from '../../datasource';
import { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish } from '../../types';
import { useMetricsMetadata } from '../metrics';
import SubscriptionField from '../SubscriptionField';
import MetricNamespaceField from './MetricNamespaceField';
import ResourceTypeField from './ResourceTypeField';
@ -15,6 +14,15 @@ import DimensionFields from './DimensionFields';
import TopField from './TopField';
import LegendFormatField from './LegendFormatField';
import { InlineFieldRow } from '@grafana/ui';
import {
useMetricNames,
useMetricNamespaces,
useResourceGroups,
useResourceNames,
useResourceTypes,
useSubscriptions,
useMetricMetadata,
} from './dataHooks';
interface MetricsQueryEditorProps {
query: AzureMonitorQuery;
@ -33,12 +41,19 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
onChange,
setError,
}) => {
const metricsMetadata = useMetricsMetadata(datasource, query, subscriptionId, onChange);
const metricsMetadata = useMetricMetadata(query, datasource, onChange);
const subscriptions = useSubscriptions(query, datasource, onChange, setError);
const resourceGroups = useResourceGroups(query, datasource, onChange, setError);
const resourceTypes = useResourceTypes(query, datasource, onChange, setError);
const resourceNames = useResourceNames(query, datasource, onChange, setError);
const metricNames = useMetricNames(query, datasource, onChange, setError);
const metricNamespaces = useMetricNamespaces(query, datasource, onChange, setError);
return (
<div data-testid="azure-monitor-metrics-query-editor">
<InlineFieldRow>
<SubscriptionField
subscriptions={subscriptions}
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
@ -48,6 +63,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
/>
<ResourceGroupsField
resourceGroups={resourceGroups}
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
@ -59,6 +75,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
<InlineFieldRow>
<ResourceTypeField
resourceTypes={resourceTypes}
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
@ -67,6 +84,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
setError={setError}
/>
<ResourceNameField
resourceNames={resourceNames}
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
@ -78,6 +96,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
<InlineFieldRow>
<MetricNamespaceField
metricNamespaces={metricNamespaces}
query={query}
datasource={datasource}
subscriptionId={subscriptionId}
@ -86,6 +105,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
setError={setError}
/>
<MetricNameField
metricNames={metricNames}
query={query}
datasource={datasource}
subscriptionId={subscriptionId}

View File

@ -1,57 +1,26 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { Field } from '../Field';
import { findOption, toOption } from '../../utils/common';
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
import { setResourceGroup } from './setQueryValue';
const ERROR_SOURCE = 'metrics-resourcegroups';
const ResourceGroupsField: React.FC<AzureQueryEditorFieldProps> = ({
interface ResourceGroupsFieldProps extends AzureQueryEditorFieldProps {
resourceGroups: AzureMonitorOption[];
}
const ResourceGroupsField: React.FC<ResourceGroupsFieldProps> = ({
query,
datasource,
subscriptionId,
resourceGroups,
variableOptionGroup,
onQueryChange,
setError,
}) => {
const [resourceGroups, setResourceGroups] = useState<AzureMonitorOption[]>([]);
useEffect(() => {
if (!subscriptionId) {
resourceGroups.length > 0 && setResourceGroups([]);
return;
}
datasource
.getResourceGroups(subscriptionId)
.then((results) => {
setResourceGroups(results.map(toOption));
setError(ERROR_SOURCE, undefined);
})
.catch((err) => setError(ERROR_SOURCE, err));
}, [datasource, resourceGroups.length, setError, subscriptionId]);
const handleChange = useCallback(
(change: SelectableValue<string>) => {
if (!change.value) {
return;
}
onQueryChange({
...query,
azureMonitor: {
...query.azureMonitor,
resourceGroup: change.value,
metricDefinition: undefined,
resourceName: undefined,
metricNamespace: undefined,
metricName: undefined,
aggregation: undefined,
timeGrain: '',
dimensionFilters: [],
},
});
const newQuery = setResourceGroup(query, change.value);
onQueryChange(newQuery);
},
[onQueryChange, query]
);
@ -62,7 +31,7 @@ const ResourceGroupsField: React.FC<AzureQueryEditorFieldProps> = ({
<Field label="Resource group">
<Select
inputId="azure-monitor-metrics-resource-group-field"
value={findOption(resourceGroups, query.azureMonitor?.resourceGroup)}
value={query.azureMonitor?.resourceGroup}
onChange={handleChange}
options={options}
width={38}

View File

@ -1,67 +1,37 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { Field } from '../Field';
import { findOption, toOption } from '../../utils/common';
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
import { setResourceName } from './setQueryValue';
const ERROR_SOURCE = 'metrics-resource';
const ResourceNameField: React.FC<AzureQueryEditorFieldProps> = ({
interface ResourceNameFieldProps extends AzureQueryEditorFieldProps {
resourceNames: AzureMonitorOption[];
}
const ResourceNameField: React.FC<ResourceNameFieldProps> = ({
resourceNames,
query,
datasource,
subscriptionId,
variableOptionGroup,
onQueryChange,
setError,
}) => {
const [resourceNames, setResourceNames] = useState<AzureMonitorOption[]>([]);
useEffect(() => {
const { resourceGroup, metricDefinition } = query.azureMonitor ?? {};
if (!(subscriptionId && resourceGroup && metricDefinition)) {
resourceNames.length > 0 && setResourceNames([]);
return;
}
datasource
.getResourceNames(subscriptionId, resourceGroup, metricDefinition)
.then((results) => setResourceNames(results.map(toOption)))
.catch((err) => setError(ERROR_SOURCE, err));
}, [datasource, query.azureMonitor, resourceNames.length, setError, subscriptionId]);
const handleChange = useCallback(
(change: SelectableValue<string>) => {
if (!change.value) {
return;
}
onQueryChange({
...query,
azureMonitor: {
...query.azureMonitor,
resourceName: change.value,
metricNamespace: undefined,
metricName: undefined,
aggregation: undefined,
timeGrain: '',
dimensionFilters: [],
},
});
const newQuery = setResourceName(query, change.value);
onQueryChange(newQuery);
},
[onQueryChange, query]
);
const options = useMemo(() => [...resourceNames, variableOptionGroup], [resourceNames, variableOptionGroup]);
const value = query.azureMonitor?.resourceName ?? null;
const selectedResourceNameValue = findOption(resourceNames, query.azureMonitor?.resourceName);
return (
<Field label="Resource name">
<Select
inputId="azure-monitor-metrics-resource-name-field"
value={selectedResourceNameValue}
value={value}
onChange={handleChange}
options={options}
width={38}

View File

@ -1,67 +1,41 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { Field } from '../Field';
import { findOption, toOption } from '../../utils/common';
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
import { setResourceType } from './setQueryValue';
const ERROR_SOURCE = 'resource-type';
const NamespaceField: React.FC<AzureQueryEditorFieldProps> = ({
interface NamespaceFieldProps extends AzureQueryEditorFieldProps {
resourceTypes: AzureMonitorOption[];
}
const NamespaceField: React.FC<NamespaceFieldProps> = ({
resourceTypes,
query,
datasource,
subscriptionId,
variableOptionGroup,
onQueryChange,
setError,
}) => {
const [namespaces, setNamespaces] = useState<AzureMonitorOption[]>([]);
useEffect(() => {
const { resourceGroup } = query.azureMonitor ?? {};
if (!(subscriptionId && resourceGroup)) {
namespaces.length && setNamespaces([]);
return;
}
datasource
.getMetricDefinitions(subscriptionId, resourceGroup)
.then((results) => setNamespaces(results.map(toOption)))
.catch((err) => setError(ERROR_SOURCE, err));
}, [datasource, namespaces.length, query.azureMonitor, setError, subscriptionId]);
const handleChange = useCallback(
(change: SelectableValue<string>) => {
if (!change.value) {
return;
}
onQueryChange({
...query,
azureMonitor: {
...query.azureMonitor,
metricDefinition: change.value,
resourceName: undefined,
metricNamespace: undefined,
metricName: undefined,
aggregation: undefined,
timeGrain: '',
dimensionFilters: [],
},
});
const newQuery = setResourceType(query, change.value);
onQueryChange(newQuery);
},
[onQueryChange, query]
);
const options = useMemo(() => [...namespaces, variableOptionGroup], [namespaces, variableOptionGroup]);
const options = useMemo(() => [...resourceTypes, variableOptionGroup], [resourceTypes, variableOptionGroup]);
return (
<Field label="Resource type">
{/* It's expected that the label reads Resource type but the property is metricDefinition */}
<Select
inputId="azure-monitor-metrics-resource-type-field"
value={findOption(namespaces, query.azureMonitor?.metricDefinition)}
value={query.azureMonitor?.metricDefinition}
onChange={handleChange}
options={options}
width={38}

View File

@ -3,9 +3,9 @@ import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { Field } from '../Field';
import { findOption } from '../../utils/common';
import TimegrainConverter from '../../time_grain_converter';
import { AzureQueryEditorFieldProps, AzureMonitorOption } from '../../types';
import { setTimeGrain } from './setQueryValue';
interface TimeGrainFieldProps extends AzureQueryEditorFieldProps {
timeGrainOptions: AzureMonitorOption[];
@ -23,13 +23,8 @@ const TimeGrainField: React.FC<TimeGrainFieldProps> = ({
return;
}
onQueryChange({
...query,
azureMonitor: {
...query.azureMonitor,
timeGrain: change.value,
},
});
const newQuery = setTimeGrain(query, change.value);
onQueryChange(newQuery);
},
[onQueryChange, query]
);
@ -58,7 +53,7 @@ const TimeGrainField: React.FC<TimeGrainFieldProps> = ({
<Field label="Time grain">
<Select
inputId="azure-monitor-metrics-time-grain-field"
value={findOption(timeGrainOptions, query.azureMonitor?.timeGrain)}
value={query.azureMonitor?.timeGrain}
onChange={handleChange}
options={timeGrains}
width={38}

View File

@ -3,6 +3,7 @@ import { Input } from '@grafana/ui';
import { Field } from '../Field';
import { AzureQueryEditorFieldProps } from '../../types';
import { setTop } from './setQueryValue';
const TopField: React.FC<AzureQueryEditorFieldProps> = ({ onQueryChange, query }) => {
const [value, setValue] = useState<string>(query.azureMonitor?.top ?? '');
@ -16,13 +17,8 @@ const TopField: React.FC<AzureQueryEditorFieldProps> = ({ onQueryChange, query }
}, []);
const handleBlur = useCallback(() => {
onQueryChange({
...query,
azureMonitor: {
...query.azureMonitor,
top: value,
},
});
const newQuery = setTop(query, value);
onQueryChange(newQuery);
}, [onQueryChange, query, value]);
return (

View File

@ -0,0 +1,66 @@
import { renderHook } from '@testing-library/react-hooks';
import { useAsyncState } from './dataHooks';
interface WaitableMock extends jest.Mock<any, any> {
waitToBeCalled(): Promise<unknown>;
}
function createWaitableMock() {
let resolve: Function;
const mock = jest.fn() as WaitableMock;
mock.mockImplementation(() => {
resolve && resolve();
});
mock.waitToBeCalled = () => {
return new Promise((_resolve) => (resolve = _resolve));
};
return mock;
}
describe('AzureMonitor: useAsyncState', () => {
const MOCKED_RANDOM_VALUE = 0.42069;
beforeEach(() => {
jest.spyOn(global.Math, 'random').mockReturnValue(MOCKED_RANDOM_VALUE);
});
afterEach(() => {
jest.spyOn(global.Math, 'random').mockRestore();
});
it('should return data from an async function', async () => {
const apiCall = () => Promise.resolve(['a', 'b', 'c']);
const setError = jest.fn();
const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
await waitForNextUpdate();
expect(result.current).toEqual(['a', 'b', 'c']);
});
it('should report errors through setError', async () => {
const error = new Error();
const apiCall = () => Promise.reject(error);
const setError = createWaitableMock();
const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]);
expect(result.current).toEqual([]);
expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, error);
});
it('should clear the error once the request is successful', async () => {
const apiCall = () => Promise.resolve(['a', 'b', 'c']);
const setError = createWaitableMock();
const { waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]);
expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, undefined);
});
});

View File

@ -0,0 +1,272 @@
import { useState, useEffect, useMemo } from 'react';
import Datasource from '../../datasource';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
import { hasOption, toOption } from '../../utils/common';
import {
setMetricName,
setMetricNamespace,
setResourceGroup,
setResourceName,
setResourceType,
setSubscriptionID,
} from './setQueryValue';
export interface MetricMetadata {
aggOptions: AzureMonitorOption[];
timeGrains: AzureMonitorOption[];
dimensions: AzureMonitorOption[];
isLoading: boolean;
// These two properties are only used within the hook, and not elsewhere
supportedAggTypes: string[];
primaryAggType: string | undefined;
}
type SetErrorFn = (source: string, error: AzureMonitorErrorish | undefined) => void;
type OnChangeFn = (newQuery: AzureMonitorQuery) => void;
type DataHook = (
query: AzureMonitorQuery,
datasource: Datasource,
onChange: OnChangeFn,
setError: SetErrorFn
) => AzureMonitorOption[];
export function useAsyncState<T>(asyncFn: () => Promise<T>, setError: Function, dependencies: unknown[]) {
// Use the lazy initial state functionality of useState to assign a random ID to the API call
// to track where errors come from. See useLastError.
const [errorSource] = useState(() => Math.random());
const [value, setValue] = useState<T>();
const finalValue = useMemo(() => value ?? [], [value]);
useEffect(() => {
asyncFn()
.then((results) => {
setValue(results);
setError(errorSource, undefined);
})
.catch((err) => {
setError(errorSource, err);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies);
return finalValue;
}
export const useSubscriptions: DataHook = (query, datasource, onChange, setError) => {
const { subscription } = query;
const defaultSubscription = datasource.azureMonitorDatasource.defaultSubscriptionId;
return useAsyncState(
async () => {
const results = await datasource.azureMonitorDatasource.getSubscriptions();
const options = results.map((v) => ({ label: v.text, value: v.value, description: v.value }));
if (!subscription && defaultSubscription && hasOption(options, defaultSubscription)) {
onChange(setSubscriptionID(query, defaultSubscription));
} else if ((!subscription && options.length) || options.length === 1) {
onChange(setSubscriptionID(query, options[0].value));
}
return options;
},
setError,
[]
);
};
export const useResourceGroups: DataHook = (query, datasource, onChange, setError) => {
const { subscription } = query;
const { resourceGroup } = query.azureMonitor ?? {};
return useAsyncState(
async () => {
if (!subscription) {
return;
}
const results = await datasource.getResourceGroups(subscription);
const options = results.map(toOption);
if (resourceGroup && !hasOption(options, resourceGroup)) {
onChange(setResourceGroup(query, undefined));
}
return options;
},
setError,
[subscription]
);
};
export const useResourceTypes: DataHook = (query, datasource, onChange, setError) => {
const { subscription } = query;
const { resourceGroup, metricDefinition } = query.azureMonitor ?? {};
return useAsyncState(
async () => {
if (!(subscription && resourceGroup)) {
return;
}
const results = await datasource.getMetricDefinitions(subscription, resourceGroup);
const options = results.map(toOption);
if (metricDefinition && !hasOption(options, metricDefinition)) {
onChange(setResourceType(query, undefined));
}
return options;
},
setError,
[subscription, resourceGroup]
);
};
export const useResourceNames: DataHook = (query, datasource, onChange, setError) => {
const { subscription } = query;
const { resourceGroup, metricDefinition, resourceName } = query.azureMonitor ?? {};
return useAsyncState(
async () => {
if (!(subscription && resourceGroup && metricDefinition)) {
return;
}
const results = await datasource.getResourceNames(subscription, resourceGroup, metricDefinition);
const options = results.map(toOption);
if (resourceName && !hasOption(options, resourceName)) {
onChange(setResourceName(query, undefined));
}
return options;
},
setError,
[subscription, resourceGroup, metricDefinition]
);
};
export const useMetricNamespaces: DataHook = (query, datasource, onChange, setError) => {
const { subscription } = query;
const { resourceGroup, metricDefinition, resourceName, metricNamespace } = query.azureMonitor ?? {};
const metricNamespaces = useAsyncState(
async () => {
if (!(subscription && resourceGroup && metricDefinition && resourceName)) {
return;
}
const results = await datasource.getMetricNamespaces(subscription, resourceGroup, metricDefinition, resourceName);
const options = results.map(toOption);
// Do some cleanup of the query state if need be
if ((!metricNamespace && options.length) || options.length === 1) {
onChange(setMetricNamespace(query, options[0].value));
} else if (options[0] && metricNamespace && !hasOption(options, metricNamespace)) {
onChange(setMetricNamespace(query, options[0].value));
}
return options;
},
setError,
[subscription, resourceGroup, metricDefinition, resourceName]
);
return metricNamespaces;
};
export const useMetricNames: DataHook = (query, datasource, onChange, setError) => {
const { subscription } = query;
const { resourceGroup, metricDefinition, resourceName, metricNamespace, metricName } = query.azureMonitor ?? {};
return useAsyncState(
async () => {
if (!(subscription && resourceGroup && metricDefinition && resourceName && metricNamespace)) {
return;
}
const results = await datasource.getMetricNames(
subscription,
resourceGroup,
metricDefinition,
resourceName,
metricNamespace
);
const options = results.map(toOption);
if (metricName && !hasOption(options, metricName)) {
onChange(setMetricName(query, undefined));
}
return options;
},
setError,
[subscription, resourceGroup, metricDefinition, resourceName, metricNamespace]
);
};
export const useMetricMetadata = (query: AzureMonitorQuery, datasource: Datasource, onChange: OnChangeFn) => {
const [metricMetadata, setMetricMetadata] = useState<MetricMetadata>({
aggOptions: [],
timeGrains: [],
dimensions: [],
isLoading: false,
supportedAggTypes: [],
primaryAggType: undefined,
});
const { subscription } = query;
const { resourceGroup, metricDefinition, resourceName, metricNamespace, metricName, aggregation, timeGrain } =
query.azureMonitor ?? {};
// Fetch new metric metadata when the fields change
useEffect(() => {
if (!(subscription && resourceGroup && metricDefinition && resourceName && metricNamespace && metricName)) {
return;
}
datasource
.getMetricMetadata(subscription, resourceGroup, metricDefinition, resourceName, metricNamespace, metricName)
.then((metadata) => {
// 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,
isLoading: false,
supportedAggTypes: metadata.supportedAggTypes ?? [],
primaryAggType: metadata.primaryAggType,
});
});
}, [datasource, subscription, resourceGroup, metricDefinition, resourceName, metricNamespace, metricName]);
// Update the query state in response to the meta data changing
useEffect(() => {
const aggregationIsValid = aggregation && metricMetadata.supportedAggTypes.includes(aggregation);
const newAggregation = aggregationIsValid ? aggregation : metricMetadata.primaryAggType;
const newTimeGrain = timeGrain || 'auto';
if (newAggregation !== aggregation || newTimeGrain !== timeGrain) {
onChange({
...query,
azureMonitor: {
...query.azureMonitor,
aggregation: newAggregation,
timeGrain: newTimeGrain,
},
});
}
}, [onChange, metricMetadata, aggregation, timeGrain, query]);
return metricMetadata;
};

View File

@ -0,0 +1,202 @@
import { AzureMetricDimension, AzureMonitorQuery } from '../../types';
export function setSubscriptionID(query: AzureMonitorQuery, subscriptionID: string): AzureMonitorQuery {
if (query.subscription === subscriptionID) {
return query;
}
return {
...query,
subscription: subscriptionID,
azureMonitor: {
...query.azureMonitor,
resourceGroup: undefined,
},
};
}
export function setResourceGroup(query: AzureMonitorQuery, resourceGroup: string | undefined): AzureMonitorQuery {
if (query.azureMonitor?.resourceGroup === resourceGroup) {
return query;
}
return {
...query,
azureMonitor: {
...query.azureMonitor,
resourceGroup: resourceGroup,
resourceName: undefined,
},
};
}
// In the query as "metricDefinition" for some reason
export function setResourceType(query: AzureMonitorQuery, resourceType: string | undefined): AzureMonitorQuery {
if (query.azureMonitor?.metricDefinition === resourceType) {
return query;
}
const newQuery = {
...query,
azureMonitor: {
...query.azureMonitor,
metricDefinition: resourceType,
resourceName: undefined,
metricNamespace: undefined,
metricName: undefined,
},
};
return newQuery;
}
export function setResourceName(query: AzureMonitorQuery, resourceName: string | undefined): AzureMonitorQuery {
if (query.azureMonitor?.resourceName === resourceName) {
return query;
}
return {
...query,
azureMonitor: {
...query.azureMonitor,
resourceName: resourceName,
},
};
}
export function setMetricNamespace(query: AzureMonitorQuery, metricNamespace: string | undefined): AzureMonitorQuery {
if (query.azureMonitor?.metricNamespace === metricNamespace) {
return query;
}
return {
...query,
azureMonitor: {
...query.azureMonitor,
metricNamespace: metricNamespace,
metricName: undefined,
},
};
}
export function setMetricName(query: AzureMonitorQuery, metricName: string | undefined): AzureMonitorQuery {
if (query.azureMonitor?.metricName === metricName) {
return query;
}
return {
...query,
azureMonitor: {
...query.azureMonitor,
metricName: metricName,
},
};
}
export function setAggregation(query: AzureMonitorQuery, aggregation: string): AzureMonitorQuery {
if (query.azureMonitor?.aggregation === aggregation) {
return query;
}
return {
...query,
azureMonitor: {
...query.azureMonitor,
aggregation: aggregation,
},
};
}
export function setTimeGrain(query: AzureMonitorQuery, timeGrain: string): AzureMonitorQuery {
if (query.azureMonitor?.timeGrain === timeGrain) {
return query;
}
return {
...query,
azureMonitor: {
...query.azureMonitor,
timeGrain: timeGrain,
},
};
}
export function setDimensionFilters(query: AzureMonitorQuery, dimensions: AzureMetricDimension[]): AzureMonitorQuery {
if (query.azureMonitor?.dimensionFilters === dimensions) {
return query;
}
return {
...query,
azureMonitor: {
...query.azureMonitor,
dimensionFilters: dimensions,
},
};
}
export function appendDimensionFilter(
query: AzureMonitorQuery,
dimension = '',
operator = 'eq',
filter = ''
): AzureMonitorQuery {
const existingFilters = query.azureMonitor?.dimensionFilters ?? [];
return setDimensionFilters(query, [
...existingFilters,
{
dimension,
operator,
filter,
},
]);
}
export function removeDimensionFilter(query: AzureMonitorQuery, indexToRemove: number): AzureMonitorQuery {
const existingFilters = query.azureMonitor?.dimensionFilters ?? [];
const newFilters = [...existingFilters];
newFilters.splice(indexToRemove, 1);
return setDimensionFilters(query, newFilters);
}
export function setDimensionFilterValue<Key extends keyof AzureMetricDimension>(
query: AzureMonitorQuery,
index: number,
fieldName: Key,
value: AzureMetricDimension[Key]
): AzureMonitorQuery {
const existingFilters = query.azureMonitor?.dimensionFilters ?? [];
const newFilters = [...existingFilters];
const newFilter = newFilters[index];
newFilter[fieldName] = value;
return setDimensionFilters(query, newFilters);
}
export function setTop(query: AzureMonitorQuery, top: string): AzureMonitorQuery {
if (query.azureMonitor?.top === top) {
return query;
}
return {
...query,
azureMonitor: {
...query.azureMonitor,
top: top,
},
};
}
export function setLegendAlias(query: AzureMonitorQuery, alias: string): AzureMonitorQuery {
if (query.azureMonitor?.alias === alias) {
return query;
}
return {
...query,
azureMonitor: {
...query.azureMonitor,
alias: alias,
},
};
}

View File

@ -3,7 +3,6 @@ import { Select } from '@grafana/ui';
import { Field } from '../Field';
import { AzureMonitorQuery, AzureQueryType } from '../../types';
import { SelectableValue } from '@grafana/data';
import { findOption } from '../../utils/common';
interface QueryTypeFieldProps {
query: AzureMonitorQuery;
@ -45,7 +44,7 @@ const QueryTypeField: React.FC<QueryTypeFieldProps> = ({ query, onQueryChange })
<Field label="Service">
<Select
inputId="azure-monitor-query-type-field"
value={findOption(queryTypes, query.queryType)}
value={query.queryType}
options={queryTypes}
onChange={handleChange}
width={38}

View File

@ -1,57 +1,24 @@
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select, MultiSelect } from '@grafana/ui';
import { AzureMonitorQuery, AzureQueryEditorFieldProps, AzureMonitorOption, AzureQueryType } from '../types';
import { findOption, findOptions } from '../utils/common';
import { findOptions } from '../utils/common';
import { Field } from './Field';
interface SubscriptionFieldProps extends AzureQueryEditorFieldProps {
onQueryChange: (newQuery: AzureMonitorQuery) => void;
subscriptions: AzureMonitorOption[];
multiSelect?: boolean;
}
const ERROR_SOURCE = 'metrics-subscription';
const SubscriptionField: React.FC<SubscriptionFieldProps> = ({
datasource,
query,
subscriptions,
variableOptionGroup,
onQueryChange,
setError,
multiSelect = false,
}) => {
const [subscriptions, setSubscriptions] = useState<AzureMonitorOption[]>([]);
useEffect(() => {
datasource.azureMonitorDatasource
.getSubscriptions()
.then((results) => {
const newSubscriptions = results.map((v) => ({ label: v.text, value: v.value, description: v.value }));
setSubscriptions(newSubscriptions);
setError(ERROR_SOURCE, undefined);
let newSubscription = query.subscription || datasource.azureMonitorDatasource.defaultSubscriptionId;
if (!newSubscription && newSubscriptions.length > 0) {
newSubscription = newSubscriptions[0].value;
}
if (newSubscription && newSubscription !== query.subscription) {
onQueryChange({
...query,
subscription: newSubscription,
});
}
})
.catch((err) => setError(ERROR_SOURCE, err));
}, [
datasource.azureMonitorDatasource?.defaultSubscriptionId,
datasource.azureMonitorDatasource,
onQueryChange,
query,
setError,
]);
const handleChange = useCallback(
(change: SelectableValue<string>) => {
if (!change.value) {
@ -111,7 +78,7 @@ const SubscriptionField: React.FC<SubscriptionFieldProps> = ({
) : (
<Field label="Subscription">
<Select
value={findOption(subscriptions, query.subscription)}
value={query.subscription}
inputId="azure-monitor-subscriptions-field"
onChange={handleChange}
options={options}

View File

@ -1,95 +0,0 @@
import { useState, useEffect } from 'react';
import Datasource from '../datasource';
import { AzureMonitorQuery } from '../types';
import { convertTimeGrainsToMs } from '../utils/common';
export interface MetricMetadata {
aggOptions: Array<{ label: string; value: string }>;
timeGrains: Array<{ label: string; value: string }>;
dimensions: Array<{ label: string; value: string }>;
isLoading: boolean;
}
export function useMetricsMetadata(
datasource: Datasource,
query: AzureMonitorQuery,
subscriptionId: string | undefined,
onQueryChange: (newQuery: AzureMonitorQuery) => void
) {
const [metricMetadata, setMetricMetadata] = useState<MetricMetadata>({
aggOptions: [],
timeGrains: [],
dimensions: [],
isLoading: false,
});
useEffect(() => {
if (
!(
subscriptionId &&
query.azureMonitor &&
query.azureMonitor.resourceGroup &&
query.azureMonitor.metricDefinition &&
query.azureMonitor.resourceName &&
query.azureMonitor.metricNamespace &&
query.azureMonitor.metricName
)
) {
return;
}
setMetricMetadata((prevState) => ({ ...prevState, isLoading: true }));
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:
query.azureMonitor?.aggregation && metadata.supportedAggTypes.includes(query.azureMonitor.aggregation)
? query.azureMonitor.aggregation
: metadata.primaryAggType,
timeGrain: query.azureMonitor?.timeGrain || 'auto', // TODO: move this default value somewhere better?
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,
isLoading: false,
});
})
.catch((err) => {
// TODO: handle error
console.error(err);
});
}, [
subscriptionId,
query.azureMonitor?.resourceGroup,
query.azureMonitor?.metricDefinition,
query.azureMonitor?.resourceName,
query.azureMonitor?.metricNamespace,
query.azureMonitor?.metricName,
query,
datasource,
onQueryChange,
]);
return metricMetadata;
}

View File

@ -75,8 +75,6 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
}
query(options: DataQueryRequest<AzureMonitorQuery>): Observable<DataQueryResponse> {
console.log('query options.targets', options.targets);
const byType = new Map<AzureQueryType, DataQueryRequest<AzureMonitorQuery>>();
for (const target of options.targets) {

View File

@ -41,7 +41,6 @@ export interface AzureMetricQuery {
metricNamespace?: string;
metricName?: string;
timeGrain?: string;
allowedTimeGrainsMs?: number[];
aggregation?: string;
dimensionFilters?: AzureMetricDimension[];
alias?: string;
@ -49,6 +48,9 @@ export interface AzureMetricQuery {
/** @deprecated */
timeGrainUnit?: string;
/** @deprecated Remove this once angular is removed */
allowedTimeGrainsMs?: number[];
}
/**
@ -100,6 +102,6 @@ export interface InsightsAnalyticsQuery {
export interface AzureMetricDimension {
dimension: string;
operator: 'eq'; // future proof
filter?: string; // *
operator: string;
filter?: string;
}

View File

@ -140,6 +140,7 @@ export interface AzureLogsTableColumn {
export interface AzureMonitorOption<T = string> {
label: string;
value: T;
options?: AzureMonitorOption[];
}
export interface AzureQueryEditorFieldProps {

View File

@ -0,0 +1,42 @@
import { hasOption } from './common';
describe('AzureMonitor: hasOption', () => {
it('can find an option in flat array', () => {
const options = [
{ value: 'a', label: 'a' },
{ value: 'b', label: 'b' },
{ value: 'c', label: 'c' },
];
expect(hasOption(options, 'b')).toBeTruthy();
});
it('can not find an option in flat array', () => {
const options = [
{ value: 'a', label: 'a' },
{ value: 'b', label: 'b' },
{ value: 'c', label: 'c' },
];
expect(hasOption(options, 'not-there')).not.toBeTruthy();
});
it('can find an option in a nested group', () => {
const options = [
{ value: 'a', label: 'a' },
{ value: 'b', label: 'b' },
{
label: 'c',
value: 'c',
options: [
{ value: 'c-a', label: 'c-a' },
{ value: 'c-b', label: 'c-b' },
{ value: 'c-c', label: 'c-c' },
],
},
{ value: 'd', label: 'd' },
];
expect(hasOption(options, 'c-b')).toBeTruthy();
});
});

View File

@ -2,9 +2,8 @@ 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 | undefined) =>
value ? options.find((v) => v.value === value) ?? { value, label: value } : null;
export const hasOption = (options: AzureMonitorOption[], value: string): boolean =>
options.some((v) => (v.options ? hasOption(v.options, value) : v.value === value));
export const findOptions = (options: AzureMonitorOption[], values: string[] = []) => {
if (values.length === 0) {