mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
f10c5020f1
commit
e530d66275
@ -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}
|
||||
|
@ -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={() => {}}
|
||||
|
@ -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}
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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 (
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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 (
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
||||
};
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
}
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -140,6 +140,7 @@ export interface AzureLogsTableColumn {
|
||||
export interface AzureMonitorOption<T = string> {
|
||||
label: string;
|
||||
value: T;
|
||||
options?: AzureMonitorOption[];
|
||||
}
|
||||
|
||||
export interface AzureQueryEditorFieldProps {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user