mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
CloudWatch: use custom variable editor (#46943)
This commit is contained in:
parent
c55be51f1e
commit
f8d11fbef9
@ -0,0 +1,141 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { select } from 'react-select-event';
|
||||
import { VariableQueryType } from '../../types';
|
||||
import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource';
|
||||
import { VariableQueryEditor, Props } from './VariableQueryEditor';
|
||||
|
||||
const defaultQuery = {
|
||||
queryType: VariableQueryType.Regions,
|
||||
namespace: '',
|
||||
region: '',
|
||||
metricName: '',
|
||||
dimensionKey: '',
|
||||
dimensionFilters: '',
|
||||
ec2Filters: '',
|
||||
instanceID: '',
|
||||
attributeName: '',
|
||||
resourceType: '',
|
||||
tags: '',
|
||||
refId: '',
|
||||
};
|
||||
|
||||
const ds = setupMockedDataSource();
|
||||
|
||||
ds.datasource.getRegions = jest.fn().mockResolvedValue([
|
||||
{ label: 'a1', value: 'a1' },
|
||||
{ label: 'b1', value: 'b1' },
|
||||
{ label: 'c1', value: 'c1' },
|
||||
]);
|
||||
ds.datasource.getNamespaces = jest.fn().mockResolvedValue([
|
||||
{ label: 'x2', value: 'x2' },
|
||||
{ label: 'y2', value: 'y2' },
|
||||
{ label: 'z2', value: 'z2' },
|
||||
]);
|
||||
ds.datasource.getMetrics = jest.fn().mockResolvedValue([
|
||||
{ label: 'h3', value: 'h3' },
|
||||
{ label: 'i3', value: 'i3' },
|
||||
{ label: 'j3', value: 'j3' },
|
||||
]);
|
||||
ds.datasource.getDimensionKeys = jest.fn().mockImplementation((namespace: string, region: string) => {
|
||||
if (region === 'a1') {
|
||||
return Promise.resolve([
|
||||
{ label: 'q4', value: 'q4' },
|
||||
{ label: 'r4', value: 'r4' },
|
||||
{ label: 's4', value: 's4' },
|
||||
]);
|
||||
}
|
||||
return Promise.resolve([{ label: 't4', value: 't4' }]);
|
||||
});
|
||||
ds.datasource.getVariables = jest.fn().mockReturnValue([]);
|
||||
|
||||
const defaultProps: Props = {
|
||||
onChange: jest.fn(),
|
||||
query: defaultQuery,
|
||||
datasource: ds.datasource,
|
||||
onRunQuery: () => {},
|
||||
};
|
||||
|
||||
describe('VariableEditor', () => {
|
||||
describe('and a new variable is created', () => {
|
||||
it('should trigger a query using the first query type in the array', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = defaultQuery;
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const querySelect = screen.queryByRole('combobox', { name: 'Query Type' });
|
||||
expect(querySelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('Regions')).toBeInTheDocument();
|
||||
// Should not render any fields besides Query Type
|
||||
const regionSelect = screen.queryByRole('combobox', { name: 'Region' });
|
||||
expect(regionSelect).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and an existing variable is edited', () => {
|
||||
it('should trigger new query using the saved query type', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.Metrics,
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
};
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const querySelect = screen.queryByRole('combobox', { name: 'Query Type' });
|
||||
expect(querySelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('Metrics')).toBeInTheDocument();
|
||||
const regionSelect = screen.queryByRole('combobox', { name: 'Region' });
|
||||
expect(regionSelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('a1')).toBeInTheDocument();
|
||||
const namespaceSelect = screen.queryByRole('combobox', { name: 'Namespace' });
|
||||
expect(namespaceSelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('z2')).toBeInTheDocument();
|
||||
// Should only render Query Type, Region, and Namespace selectors
|
||||
const metricSelect = screen.queryByRole('combobox', { name: 'Metric' });
|
||||
expect(metricSelect).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('and a different region is selected', () => {
|
||||
it('should clear invalid fields', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.DimensionValues,
|
||||
namespace: 'z2',
|
||||
region: 'a1',
|
||||
metricName: 'i3',
|
||||
dimensionKey: 's4',
|
||||
};
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
const querySelect = screen.queryByLabelText('Query Type');
|
||||
expect(querySelect).toBeInTheDocument();
|
||||
expect(screen.queryByText('Dimension Values')).toBeInTheDocument();
|
||||
const regionSelect = screen.getByRole('combobox', { name: 'Region' });
|
||||
regionSelect.click();
|
||||
await select(regionSelect, 'b1', {
|
||||
container: document.body,
|
||||
});
|
||||
|
||||
expect(ds.datasource.getMetrics).toHaveBeenCalledWith('z2', 'b1');
|
||||
expect(ds.datasource.getDimensionKeys).toHaveBeenCalledWith('z2', 'b1');
|
||||
expect(props.onChange).toHaveBeenCalledWith({
|
||||
...defaultQuery,
|
||||
refId: 'CloudWatchVariableQueryEditor-VariableQuery',
|
||||
queryType: VariableQueryType.DimensionValues,
|
||||
namespace: 'z2',
|
||||
region: 'b1',
|
||||
// metricName i3 exists in the new region and should not be removed
|
||||
metricName: 'i3',
|
||||
// dimensionKey s4 does not exist in the new region and should be removed
|
||||
dimensionKey: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,181 @@
|
||||
import React from 'react';
|
||||
|
||||
import { QueryEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { VariableTextField } from './VariableTextField';
|
||||
import { CloudWatchDatasource } from '../../datasource';
|
||||
import { useDimensionKeys, useMetrics, useNamespaces, useRegions } from '../../hooks';
|
||||
import { CloudWatchJsonData, CloudWatchQuery, VariableQuery, VariableQueryType } from '../../types';
|
||||
import { migrateVariableQuery } from '../../migrations';
|
||||
import { VariableQueryField } from './VariableQueryField';
|
||||
|
||||
export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData, VariableQuery>;
|
||||
|
||||
const queryTypes: Array<{ value: string; label: string }> = [
|
||||
{ value: VariableQueryType.Regions, label: 'Regions' },
|
||||
{ value: VariableQueryType.Namespaces, label: 'Namespaces' },
|
||||
{ value: VariableQueryType.Metrics, label: 'Metrics' },
|
||||
{ value: VariableQueryType.DimensionKeys, label: 'Dimension Keys' },
|
||||
{ value: VariableQueryType.DimensionValues, label: 'Dimension Values' },
|
||||
{ value: VariableQueryType.EBSVolumeIDs, label: 'EBS Volume IDs' },
|
||||
{ value: VariableQueryType.EC2InstanceAttributes, label: 'EC2 Instance Attributes' },
|
||||
{ value: VariableQueryType.ResourceArns, label: 'Resource ARNs' },
|
||||
{ value: VariableQueryType.Statistics, label: 'Statistics' },
|
||||
];
|
||||
|
||||
export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
const parsedQuery = migrateVariableQuery(query);
|
||||
|
||||
const { region, namespace, metricName, dimensionKey } = parsedQuery;
|
||||
const [regions, regionIsLoading] = useRegions(datasource);
|
||||
const namespaces = useNamespaces(datasource);
|
||||
const metrics = useMetrics(datasource, region, namespace);
|
||||
const dimensionKeys = useDimensionKeys(datasource, region, namespace, metricName);
|
||||
|
||||
const onRegionChange = async (region: string) => {
|
||||
const validatedQuery = await sanitizeQuery({
|
||||
...parsedQuery,
|
||||
region,
|
||||
});
|
||||
onQueryChange(validatedQuery);
|
||||
};
|
||||
|
||||
const onNamespaceChange = async (namespace: string) => {
|
||||
const validatedQuery = await sanitizeQuery({
|
||||
...parsedQuery,
|
||||
namespace,
|
||||
});
|
||||
onQueryChange(validatedQuery);
|
||||
};
|
||||
|
||||
const onQueryChange = (newQuery: VariableQuery) => {
|
||||
onChange({ ...newQuery, refId: 'CloudWatchVariableQueryEditor-VariableQuery' });
|
||||
};
|
||||
|
||||
// Reset dimensionValue parameters if namespace or region change
|
||||
const sanitizeQuery = async (query: VariableQuery) => {
|
||||
let { metricName, dimensionKey, dimensionFilters, namespace, region } = query;
|
||||
if (metricName) {
|
||||
await datasource.getMetrics(namespace, region).then((result: Array<SelectableValue<string>>) => {
|
||||
if (!result.find((metric) => metric.value === metricName)) {
|
||||
metricName = '';
|
||||
dimensionFilters = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
if (dimensionKey) {
|
||||
await datasource.getDimensionKeys(namespace, region).then((result: Array<SelectableValue<string>>) => {
|
||||
if (!result.find((key) => key.value === dimensionKey)) {
|
||||
dimensionKey = '';
|
||||
dimensionFilters = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
return { ...query, metricName, dimensionKey, dimensionFilters };
|
||||
};
|
||||
|
||||
const hasRegionField = [
|
||||
VariableQueryType.Metrics,
|
||||
VariableQueryType.DimensionKeys,
|
||||
VariableQueryType.DimensionValues,
|
||||
VariableQueryType.EBSVolumeIDs,
|
||||
VariableQueryType.EC2InstanceAttributes,
|
||||
VariableQueryType.ResourceArns,
|
||||
].includes(parsedQuery.queryType);
|
||||
const hasNamespaceField = [
|
||||
VariableQueryType.Metrics,
|
||||
VariableQueryType.DimensionKeys,
|
||||
VariableQueryType.DimensionValues,
|
||||
].includes(parsedQuery.queryType);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VariableQueryField
|
||||
value={parsedQuery.queryType}
|
||||
options={queryTypes}
|
||||
onChange={(value: VariableQueryType) => onQueryChange({ ...parsedQuery, queryType: value })}
|
||||
label="Query Type"
|
||||
/>
|
||||
{hasRegionField && (
|
||||
<VariableQueryField
|
||||
value={region}
|
||||
options={regions}
|
||||
onChange={(value: string) => onRegionChange(value)}
|
||||
label="Region"
|
||||
isLoading={regionIsLoading}
|
||||
/>
|
||||
)}
|
||||
{hasNamespaceField && (
|
||||
<VariableQueryField
|
||||
value={namespace}
|
||||
options={namespaces}
|
||||
onChange={(value: string) => onNamespaceChange(value)}
|
||||
label="Namespace"
|
||||
/>
|
||||
)}
|
||||
{parsedQuery.queryType === VariableQueryType.DimensionValues && (
|
||||
<>
|
||||
<VariableQueryField
|
||||
value={metricName || null}
|
||||
options={metrics}
|
||||
onChange={(value: string) => onQueryChange({ ...parsedQuery, metricName: value })}
|
||||
label="Metric"
|
||||
/>
|
||||
<VariableQueryField
|
||||
value={dimensionKey || null}
|
||||
options={dimensionKeys}
|
||||
onChange={(value: string) => onQueryChange({ ...parsedQuery, dimensionKey: value })}
|
||||
label="Dimension Key"
|
||||
/>
|
||||
<VariableTextField
|
||||
value={query.dimensionFilters}
|
||||
tooltip='A JSON object representing dimensions and the values to filter on. Ex. { "filter_name1": [ "filter_value1" ], "filter_name2": [ "*" ] }'
|
||||
placeholder='{"key":["value"]}'
|
||||
onBlur={(value: string) => onQueryChange({ ...parsedQuery, dimensionFilters: value })}
|
||||
label="Filters"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{parsedQuery.queryType === VariableQueryType.EBSVolumeIDs && (
|
||||
<VariableTextField
|
||||
value={query.instanceID}
|
||||
placeholder="i-XXXXXXXXXXXXXXXXX"
|
||||
onBlur={(value: string) => onQueryChange({ ...parsedQuery, instanceID: value })}
|
||||
label="Instance ID"
|
||||
/>
|
||||
)}
|
||||
{parsedQuery.queryType === VariableQueryType.EC2InstanceAttributes && (
|
||||
<>
|
||||
<VariableTextField
|
||||
value={parsedQuery.attributeName}
|
||||
placeholder="attribute name"
|
||||
onBlur={(value: string) => onQueryChange({ ...parsedQuery, attributeName: value })}
|
||||
label="Attribute Name"
|
||||
/>
|
||||
<VariableTextField
|
||||
value={parsedQuery.ec2Filters}
|
||||
tooltip='A JSON object representing dimensions/tags and the values to filter on. Ex. { "filter_name": [ "filter_value" ], "tag:name": [ "*" ] }'
|
||||
placeholder='{"key":["value"]}'
|
||||
onBlur={(value: string) => onQueryChange({ ...parsedQuery, ec2Filters: value })}
|
||||
label="Filters"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{parsedQuery.queryType === VariableQueryType.ResourceArns && (
|
||||
<>
|
||||
<VariableTextField
|
||||
value={parsedQuery.resourceType}
|
||||
placeholder="resource type"
|
||||
onBlur={(value: string) => onQueryChange({ ...parsedQuery, resourceType: value })}
|
||||
label="Resource Type"
|
||||
/>
|
||||
<VariableTextField
|
||||
value={parsedQuery.tags}
|
||||
placeholder='{"tag":["value"]}'
|
||||
onBlur={(value: string) => onQueryChange({ ...parsedQuery, tags: value })}
|
||||
label="Tags"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,41 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { InlineField, Select } from '@grafana/ui';
|
||||
import React from 'react';
|
||||
import { VariableQueryType } from '../../types';
|
||||
|
||||
const LABEL_WIDTH = 20;
|
||||
|
||||
interface VariableQueryFieldProps<T> {
|
||||
onChange: (value: T) => void;
|
||||
options: SelectableValue[];
|
||||
value: T | null;
|
||||
label: string;
|
||||
allowCustomValue?: boolean;
|
||||
isLoading?: boolean;
|
||||
inputId?: string;
|
||||
}
|
||||
|
||||
export const VariableQueryField = <T extends string | VariableQueryType>({
|
||||
label,
|
||||
onChange,
|
||||
value,
|
||||
options,
|
||||
allowCustomValue = false,
|
||||
isLoading = false,
|
||||
}: VariableQueryFieldProps<T>) => {
|
||||
return (
|
||||
<InlineField label={label} labelWidth={LABEL_WIDTH} htmlFor={'inline-field'}>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
aria-label={label}
|
||||
width={25}
|
||||
allowCustomValue={allowCustomValue}
|
||||
value={value}
|
||||
onChange={({ value }) => onChange(value!)}
|
||||
options={options}
|
||||
isLoading={isLoading}
|
||||
inputId="inline-field"
|
||||
/>
|
||||
</InlineField>
|
||||
);
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import { InlineField, Input } from '@grafana/ui';
|
||||
import React, { FC, useState } from 'react';
|
||||
|
||||
const LABEL_WIDTH = 20;
|
||||
const TEXT_WIDTH = 100;
|
||||
|
||||
interface VariableTextFieldProps {
|
||||
onBlur: (value: string) => void;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export const VariableTextField: FC<VariableTextFieldProps> = ({ label, onBlur, placeholder, value, tooltip }) => {
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
return (
|
||||
<InlineField label={label} labelWidth={LABEL_WIDTH} tooltip={tooltip}>
|
||||
<Input
|
||||
aria-label={label}
|
||||
placeholder={placeholder}
|
||||
value={localValue}
|
||||
onChange={(e) => setLocalValue(e.currentTarget.value)}
|
||||
onBlur={() => onBlur(localValue)}
|
||||
width={TEXT_WIDTH}
|
||||
/>
|
||||
</InlineField>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { VariableQueryEditor } from './VariableQueryEditor';
|
@ -59,6 +59,7 @@ import {
|
||||
import { addDataLinksToLogsResponse } from './utils/datalinks';
|
||||
import { runWithRetry } from './utils/logsRetry';
|
||||
import { increasingInterval } from './utils/rxjs/increasingInterval';
|
||||
import { CloudWatchVariableSupport } from './variables';
|
||||
|
||||
const DS_QUERY_ENDPOINT = '/api/ds/query';
|
||||
|
||||
@ -122,6 +123,7 @@ export class CloudWatchDatasource
|
||||
this.logsTimeout = instanceSettings.jsonData.logsTimeout || '15m';
|
||||
this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this, this.templateSrv);
|
||||
this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this, this.templateSrv);
|
||||
this.variables = new CloudWatchVariableSupport(this);
|
||||
}
|
||||
|
||||
query(options: DataQueryRequest<CloudWatchQuery>): Observable<DataQueryResponse> {
|
||||
@ -735,83 +737,6 @@ export class CloudWatchDatasource
|
||||
});
|
||||
}
|
||||
|
||||
async metricFindQuery(query: string) {
|
||||
let region;
|
||||
let namespace;
|
||||
let metricName;
|
||||
let filterJson;
|
||||
|
||||
const regionQuery = query.match(/^regions\(\)/);
|
||||
if (regionQuery) {
|
||||
return this.getRegions();
|
||||
}
|
||||
|
||||
const namespaceQuery = query.match(/^namespaces\(\)/);
|
||||
if (namespaceQuery) {
|
||||
return this.getNamespaces();
|
||||
}
|
||||
|
||||
const metricNameQuery = query.match(/^metrics\(([^\)]+?)(,\s?([^,]+?))?\)/);
|
||||
if (metricNameQuery) {
|
||||
namespace = metricNameQuery[1];
|
||||
region = metricNameQuery[3];
|
||||
return this.getMetrics(namespace, region);
|
||||
}
|
||||
|
||||
const dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)(,\s?([^,]+?))?\)/);
|
||||
if (dimensionKeysQuery) {
|
||||
namespace = dimensionKeysQuery[1];
|
||||
region = dimensionKeysQuery[3];
|
||||
return this.getDimensionKeys(namespace, region);
|
||||
}
|
||||
|
||||
const dimensionValuesQuery = query.match(
|
||||
/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)(,\s?(.+))?\)/
|
||||
);
|
||||
if (dimensionValuesQuery) {
|
||||
region = dimensionValuesQuery[1];
|
||||
namespace = dimensionValuesQuery[2];
|
||||
metricName = dimensionValuesQuery[3];
|
||||
const dimensionKey = dimensionValuesQuery[4];
|
||||
filterJson = {};
|
||||
if (dimensionValuesQuery[6]) {
|
||||
filterJson = JSON.parse(this.templateSrv.replace(dimensionValuesQuery[6]));
|
||||
}
|
||||
|
||||
return this.getDimensionValues(region, namespace, metricName, dimensionKey, filterJson);
|
||||
}
|
||||
|
||||
const ebsVolumeIdsQuery = query.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/);
|
||||
if (ebsVolumeIdsQuery) {
|
||||
region = ebsVolumeIdsQuery[1];
|
||||
const instanceId = ebsVolumeIdsQuery[2];
|
||||
return this.getEbsVolumeIds(region, instanceId);
|
||||
}
|
||||
|
||||
const ec2InstanceAttributeQuery = query.match(/^ec2_instance_attribute\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
|
||||
if (ec2InstanceAttributeQuery) {
|
||||
region = ec2InstanceAttributeQuery[1];
|
||||
const targetAttributeName = ec2InstanceAttributeQuery[2];
|
||||
filterJson = JSON.parse(this.templateSrv.replace(ec2InstanceAttributeQuery[3]));
|
||||
return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson);
|
||||
}
|
||||
|
||||
const resourceARNsQuery = query.match(/^resource_arns\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
|
||||
if (resourceARNsQuery) {
|
||||
region = resourceARNsQuery[1];
|
||||
const resourceType = resourceARNsQuery[2];
|
||||
const tagsJSON = JSON.parse(this.templateSrv.replace(resourceARNsQuery[3]));
|
||||
return this.getResourceARNs(region, resourceType, tagsJSON);
|
||||
}
|
||||
|
||||
const statsQuery = query.match(/^statistics\(\)/);
|
||||
if (statsQuery) {
|
||||
return this.standardStatistics.map((s: string) => ({ value: s, label: s, text: s }));
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
annotationQuery(options: any) {
|
||||
const annotation = options.annotation;
|
||||
const statistic = this.templateSrv.replace(annotation.statistic);
|
||||
|
@ -3,8 +3,15 @@ import {
|
||||
migrateMultipleStatsAnnotationQuery,
|
||||
migrateMultipleStatsMetricsQuery,
|
||||
migrateCloudWatchQuery,
|
||||
migrateVariableQuery,
|
||||
} from './migrations';
|
||||
import { CloudWatchAnnotationQuery, CloudWatchMetricsQuery, MetricQueryType, MetricEditorMode } from './types';
|
||||
import {
|
||||
CloudWatchAnnotationQuery,
|
||||
CloudWatchMetricsQuery,
|
||||
MetricQueryType,
|
||||
MetricEditorMode,
|
||||
VariableQueryType,
|
||||
} from './types';
|
||||
|
||||
describe('migration', () => {
|
||||
describe('migrateMultipleStatsMetricsQuery', () => {
|
||||
@ -170,4 +177,51 @@ describe('migration', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('migrateVariableQuery', () => {
|
||||
describe('when metrics query is used', () => {
|
||||
describe('and region param is left out', () => {
|
||||
it('should leave an empty region', () => {
|
||||
const query = migrateVariableQuery('metrics(testNamespace)');
|
||||
expect(query.queryType).toBe(VariableQueryType.Metrics);
|
||||
expect(query.namespace).toBe('testNamespace');
|
||||
expect(query.region).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and region param is defined by user', () => {
|
||||
it('should use the user defined region', () => {
|
||||
const query = migrateVariableQuery('metrics(testNamespace2, custom-region)');
|
||||
expect(query.queryType).toBe(VariableQueryType.Metrics);
|
||||
expect(query.namespace).toBe('testNamespace2');
|
||||
expect(query.region).toBe('custom-region');
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when dimension_values query is used', () => {
|
||||
describe('and filter param is left out', () => {
|
||||
it('should leave an empty filter', () => {
|
||||
const query = migrateVariableQuery('dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)');
|
||||
expect(query.queryType).toBe(VariableQueryType.DimensionValues);
|
||||
expect(query.region).toBe('us-east-1');
|
||||
expect(query.namespace).toBe('AWS/RDS');
|
||||
expect(query.metricName).toBe('CPUUtilization');
|
||||
expect(query.dimensionKey).toBe('DBInstanceIdentifier');
|
||||
expect(query.dimensionFilters).toBe('');
|
||||
});
|
||||
});
|
||||
describe('and filter param is defined by user', () => {
|
||||
it('should use the user defined filter', () => {
|
||||
const query = migrateVariableQuery(
|
||||
'dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier,{"InstanceId":"$instance_id"})'
|
||||
);
|
||||
expect(query.queryType).toBe(VariableQueryType.DimensionValues);
|
||||
expect(query.region).toBe('us-east-1');
|
||||
expect(query.namespace).toBe('AWS/RDS');
|
||||
expect(query.metricName).toBe('CPUUtilization');
|
||||
expect(query.dimensionKey).toBe('DBInstanceIdentifier');
|
||||
expect(query.dimensionFilters).toBe('{"InstanceId":"$instance_id"}');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { AnnotationQuery, DataQuery } from '@grafana/data';
|
||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||
import { MetricEditorMode, CloudWatchAnnotationQuery, CloudWatchMetricsQuery, MetricQueryType } from './types';
|
||||
import {
|
||||
MetricEditorMode,
|
||||
CloudWatchAnnotationQuery,
|
||||
CloudWatchMetricsQuery,
|
||||
MetricQueryType,
|
||||
VariableQuery,
|
||||
VariableQueryType,
|
||||
} from './types';
|
||||
|
||||
// Migrates a metric query that use more than one statistic into multiple queries
|
||||
// E.g query.statistics = ['Max', 'Min'] will be migrated to two queries - query1.statistic = 'Max' and query2.statistic = 'Min'
|
||||
@ -61,3 +68,94 @@ export function migrateCloudWatchQuery(query: CloudWatchMetricsQuery) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function migrateVariableQuery(rawQuery: string | VariableQuery): VariableQuery {
|
||||
if (typeof rawQuery !== 'string') {
|
||||
return rawQuery;
|
||||
}
|
||||
const newQuery: VariableQuery = {
|
||||
refId: 'CloudWatchVariableQueryEditor-VariableQuery',
|
||||
queryType: VariableQueryType.Regions,
|
||||
namespace: '',
|
||||
region: '',
|
||||
metricName: '',
|
||||
dimensionKey: '',
|
||||
dimensionFilters: '',
|
||||
ec2Filters: '',
|
||||
instanceID: '',
|
||||
attributeName: '',
|
||||
resourceType: '',
|
||||
tags: '',
|
||||
};
|
||||
if (rawQuery === '') {
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
if (rawQuery.match(/^regions\(\)/)) {
|
||||
return newQuery;
|
||||
}
|
||||
if (rawQuery.match(/^namespaces\(\)/)) {
|
||||
newQuery.queryType = VariableQueryType.Namespaces;
|
||||
return newQuery;
|
||||
}
|
||||
const metricNameQuery = rawQuery.match(/^metrics\(([^\)]+?)(,\s?([^,]+?))?\)/);
|
||||
if (metricNameQuery) {
|
||||
newQuery.queryType = VariableQueryType.Metrics;
|
||||
newQuery.namespace = metricNameQuery[1];
|
||||
newQuery.region = metricNameQuery[3] || '';
|
||||
return newQuery;
|
||||
}
|
||||
const dimensionKeysQuery = rawQuery.match(/^dimension_keys\(([^\)]+?)(,\s?([^,]+?))?\)/);
|
||||
if (dimensionKeysQuery) {
|
||||
newQuery.queryType = VariableQueryType.DimensionKeys;
|
||||
newQuery.namespace = dimensionKeysQuery[1];
|
||||
newQuery.region = dimensionKeysQuery[3] || '';
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
const dimensionValuesQuery = rawQuery.match(
|
||||
/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)(,\s?(.+))?\)/
|
||||
);
|
||||
if (dimensionValuesQuery) {
|
||||
newQuery.queryType = VariableQueryType.DimensionValues;
|
||||
newQuery.region = dimensionValuesQuery[1];
|
||||
newQuery.namespace = dimensionValuesQuery[2];
|
||||
newQuery.metricName = dimensionValuesQuery[3];
|
||||
newQuery.dimensionKey = dimensionValuesQuery[4];
|
||||
newQuery.dimensionFilters = dimensionValuesQuery[6] || '';
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
const ebsVolumeIdsQuery = rawQuery.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/);
|
||||
if (ebsVolumeIdsQuery) {
|
||||
newQuery.queryType = VariableQueryType.EBSVolumeIDs;
|
||||
newQuery.region = ebsVolumeIdsQuery[1];
|
||||
newQuery.instanceID = ebsVolumeIdsQuery[2];
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
const ec2InstanceAttributeQuery = rawQuery.match(/^ec2_instance_attribute\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
|
||||
if (ec2InstanceAttributeQuery) {
|
||||
newQuery.queryType = VariableQueryType.EC2InstanceAttributes;
|
||||
newQuery.region = ec2InstanceAttributeQuery[1];
|
||||
newQuery.attributeName = ec2InstanceAttributeQuery[2];
|
||||
newQuery.ec2Filters = ec2InstanceAttributeQuery[3] || '';
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
const resourceARNsQuery = rawQuery.match(/^resource_arns\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
|
||||
if (resourceARNsQuery) {
|
||||
newQuery.queryType = VariableQueryType.ResourceArns;
|
||||
newQuery.region = resourceARNsQuery[1];
|
||||
newQuery.resourceType = resourceARNsQuery[2];
|
||||
newQuery.tags = JSON.parse(resourceARNsQuery[3]) || '';
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
const statsQuery = rawQuery.match(/^statistics\(\)/);
|
||||
if (statsQuery) {
|
||||
newQuery.queryType = VariableQueryType.Statistics;
|
||||
return newQuery;
|
||||
}
|
||||
throw new Error('unable to parse old variable query');
|
||||
}
|
||||
|
@ -506,35 +506,6 @@ describe('CloudWatchDatasource', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when regions query is used', () => {
|
||||
describe('and region param is left out', () => {
|
||||
it('should use the default region', async () => {
|
||||
const { ds, instanceSettings } = getTestContext();
|
||||
ds.doMetricResourceRequest = jest.fn().mockResolvedValue([]);
|
||||
|
||||
await ds.metricFindQuery('metrics(testNamespace)');
|
||||
|
||||
expect(ds.doMetricResourceRequest).toHaveBeenCalledWith('metrics', {
|
||||
namespace: 'testNamespace',
|
||||
region: instanceSettings.jsonData.defaultRegion,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and region param is defined by user', () => {
|
||||
it('should use the user defined region', async () => {
|
||||
const { ds } = getTestContext();
|
||||
ds.doMetricResourceRequest = jest.fn().mockResolvedValue([]);
|
||||
|
||||
await ds.metricFindQuery('metrics(testNamespace2, custom-region)');
|
||||
|
||||
expect(ds.doMetricResourceRequest).toHaveBeenCalledWith('metrics', {
|
||||
namespace: 'testNamespace2',
|
||||
region: 'custom-region',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When query region is "default"', () => {
|
||||
|
@ -367,3 +367,29 @@ export interface MetricQuery {
|
||||
maxDataPoints?: number;
|
||||
intervalMs?: number;
|
||||
}
|
||||
|
||||
export enum VariableQueryType {
|
||||
Regions = 'regions',
|
||||
Namespaces = 'namespaces',
|
||||
Metrics = 'metrics',
|
||||
DimensionKeys = 'dimensionKeys',
|
||||
DimensionValues = 'dimensionValues',
|
||||
EBSVolumeIDs = 'ebsVolumeIDs',
|
||||
EC2InstanceAttributes = 'ec2InstanceAttributes',
|
||||
ResourceArns = 'resourceARNs',
|
||||
Statistics = 'statistics',
|
||||
}
|
||||
|
||||
export interface VariableQuery extends DataQuery {
|
||||
queryType: VariableQueryType;
|
||||
namespace: string;
|
||||
region: string;
|
||||
metricName: string;
|
||||
dimensionKey: string;
|
||||
dimensionFilters: string;
|
||||
ec2Filters: string;
|
||||
instanceID: string;
|
||||
attributeName: string;
|
||||
resourceType: string;
|
||||
tags: string;
|
||||
}
|
||||
|
157
public/app/plugins/datasource/cloudwatch/variables.ts
Normal file
157
public/app/plugins/datasource/cloudwatch/variables.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { from, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { CustomVariableSupport, DataQueryRequest, DataQueryResponse } from '@grafana/data';
|
||||
|
||||
import { CloudWatchDatasource } from './datasource';
|
||||
import { VariableQuery, VariableQueryType } from './types';
|
||||
import { migrateVariableQuery } from './migrations';
|
||||
import { VariableQueryEditor } from './components/VariableQueryEditor/VariableQueryEditor';
|
||||
|
||||
export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchDatasource, VariableQuery> {
|
||||
private readonly datasource: CloudWatchDatasource;
|
||||
|
||||
constructor(datasource: CloudWatchDatasource) {
|
||||
super();
|
||||
this.datasource = datasource;
|
||||
this.query = this.query.bind(this);
|
||||
}
|
||||
|
||||
editor = VariableQueryEditor;
|
||||
|
||||
query(request: DataQueryRequest<VariableQuery>): Observable<DataQueryResponse> {
|
||||
const queryObj = migrateVariableQuery(request.targets[0]);
|
||||
return from(this.execute(queryObj)).pipe(map((data) => ({ data })));
|
||||
}
|
||||
|
||||
async execute(query: VariableQuery) {
|
||||
try {
|
||||
switch (query.queryType) {
|
||||
case VariableQueryType.Regions:
|
||||
return this.handleRegionsQuery();
|
||||
case VariableQueryType.Namespaces:
|
||||
return this.handleNamespacesQuery();
|
||||
case VariableQueryType.Metrics:
|
||||
return this.handleMetricsQuery(query);
|
||||
case VariableQueryType.DimensionKeys:
|
||||
return this.handleDimensionKeysQuery(query);
|
||||
case VariableQueryType.DimensionValues:
|
||||
return this.handleDimensionValuesQuery(query);
|
||||
case VariableQueryType.EBSVolumeIDs:
|
||||
return this.handleEbsVolumeIdsQuery(query);
|
||||
case VariableQueryType.EC2InstanceAttributes:
|
||||
return this.handleEc2InstanceAttributeQuery(query);
|
||||
case VariableQueryType.ResourceArns:
|
||||
return this.handleResourceARNsQuery(query);
|
||||
case VariableQueryType.Statistics:
|
||||
return this.handleStatisticsQuery();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Could not run CloudWatchMetricFindQuery ${query}`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async handleRegionsQuery() {
|
||||
const regions = await this.datasource.getRegions();
|
||||
return regions.map((s: { label: string; value: string }) => ({
|
||||
text: s.label,
|
||||
value: s.value,
|
||||
expandable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
async handleNamespacesQuery() {
|
||||
const namespaces = await this.datasource.getNamespaces();
|
||||
return namespaces.map((s: { label: string; value: string }) => ({
|
||||
text: s.label,
|
||||
value: s.value,
|
||||
expandable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
async handleMetricsQuery({ namespace, region }: VariableQuery) {
|
||||
const metrics = await this.datasource.getMetrics(namespace, region);
|
||||
return metrics.map((s: { label: string; value: string }) => ({
|
||||
text: s.label,
|
||||
value: s.value,
|
||||
expandable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
async handleDimensionKeysQuery({ namespace, region }: VariableQuery) {
|
||||
const keys = await this.datasource.getDimensionKeys(namespace, region);
|
||||
return keys.map((s: { label: string; value: string }) => ({
|
||||
text: s.label,
|
||||
value: s.value,
|
||||
expandable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
async handleDimensionValuesQuery({ namespace, region, dimensionKey, metricName, dimensionFilters }: VariableQuery) {
|
||||
if (!dimensionKey || !metricName) {
|
||||
return [];
|
||||
}
|
||||
var filterJson = {};
|
||||
if (dimensionFilters) {
|
||||
filterJson = JSON.parse(dimensionFilters);
|
||||
}
|
||||
const keys = await this.datasource.getDimensionValues(region, namespace, metricName, dimensionKey, filterJson);
|
||||
return keys.map((s: { label: string; value: string }) => ({
|
||||
text: s.label,
|
||||
value: s.value,
|
||||
expandable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
async handleEbsVolumeIdsQuery({ region, instanceID }: VariableQuery) {
|
||||
if (!instanceID) {
|
||||
return [];
|
||||
}
|
||||
const ids = await this.datasource.getEbsVolumeIds(region, instanceID);
|
||||
return ids.map((s: { label: string; value: string }) => ({
|
||||
text: s.label,
|
||||
value: s.value,
|
||||
expandable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
async handleEc2InstanceAttributeQuery({ region, attributeName, ec2Filters }: VariableQuery) {
|
||||
if (!attributeName) {
|
||||
return [];
|
||||
}
|
||||
var filterJson = {};
|
||||
if (ec2Filters) {
|
||||
filterJson = JSON.parse(ec2Filters);
|
||||
}
|
||||
const values = await this.datasource.getEc2InstanceAttribute(region, attributeName, filterJson);
|
||||
return values.map((s: { label: string; value: string }) => ({
|
||||
text: s.label,
|
||||
value: s.value,
|
||||
expandable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
async handleResourceARNsQuery({ region, resourceType, tags }: VariableQuery) {
|
||||
if (!resourceType) {
|
||||
return [];
|
||||
}
|
||||
var tagJson = {};
|
||||
if (tags) {
|
||||
tagJson = JSON.parse(tags);
|
||||
}
|
||||
const keys = await this.datasource.getResourceARNs(region, resourceType, tagJson);
|
||||
return keys.map((s: { label: string; value: string }) => ({
|
||||
text: s.label,
|
||||
value: s.value,
|
||||
expandable: true,
|
||||
}));
|
||||
}
|
||||
|
||||
async handleStatisticsQuery() {
|
||||
return this.datasource.standardStatistics.map((s: string) => ({
|
||||
text: s,
|
||||
value: s,
|
||||
expandable: true,
|
||||
}));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user