CloudWatch: add generic filter component to variable editor
This commit is contained in:
@@ -4,9 +4,13 @@ import { Icon } from '../Icon/Icon';
|
||||
|
||||
interface DropdownIndicatorProps {
|
||||
isOpen: boolean;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export const DropdownIndicator: React.FC<DropdownIndicatorProps> = ({ isOpen }) => {
|
||||
export const DropdownIndicator: React.FC<DropdownIndicatorProps> = ({ isOpen, hidden = false }) => {
|
||||
if (hidden) {
|
||||
return <></>;
|
||||
}
|
||||
const icon = isOpen ? 'angle-up' : 'angle-down';
|
||||
return <Icon name={icon} />;
|
||||
};
|
||||
|
||||
@@ -102,6 +102,7 @@ export function SelectBase<T>({
|
||||
formatCreateLabel,
|
||||
getOptionLabel,
|
||||
getOptionValue,
|
||||
hideIndicator,
|
||||
inputValue,
|
||||
invalid,
|
||||
isClearable = false,
|
||||
@@ -332,7 +333,7 @@ export function SelectBase<T>({
|
||||
);
|
||||
},
|
||||
DropdownIndicator(props: any) {
|
||||
return <DropdownIndicator isOpen={props.selectProps.menuIsOpen} />;
|
||||
return <DropdownIndicator isOpen={props.selectProps.menuIsOpen} hidden={hideIndicator} />;
|
||||
},
|
||||
SingleValue(props: any) {
|
||||
return <SingleValue {...props} disabled={disabled} />;
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface SelectCommonProps<T> {
|
||||
formatCreateLabel?: (input: string) => string;
|
||||
getOptionLabel?: (item: SelectableValue<T>) => React.ReactNode;
|
||||
getOptionValue?: (item: SelectableValue<T>) => string;
|
||||
hideIndicator?: boolean;
|
||||
inputValue?: string;
|
||||
invalid?: boolean;
|
||||
isClearable?: boolean;
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MultiFilter } from './MultiFilter';
|
||||
|
||||
describe('MultiFilters', () => {
|
||||
describe('when rendered with two existing multifilters', () => {
|
||||
it('should render two filter items', async () => {
|
||||
const filters = {
|
||||
InstanceId: ['a', 'b'],
|
||||
InstanceGroup: ['Group1'],
|
||||
};
|
||||
const onChange = jest.fn();
|
||||
render(<MultiFilter filters={filters} onChange={onChange} />);
|
||||
const filterItems = screen.getAllByTestId('cloudwatch-multifilter-item');
|
||||
expect(filterItems.length).toBe(2);
|
||||
|
||||
expect(within(filterItems[0]).getByDisplayValue('InstanceId')).toBeInTheDocument();
|
||||
expect(within(filterItems[0]).getByText('a')).toBeInTheDocument();
|
||||
expect(within(filterItems[0]).getByText('b')).toBeInTheDocument();
|
||||
|
||||
expect(within(filterItems[1]).getByDisplayValue('InstanceGroup')).toBeInTheDocument();
|
||||
expect(within(filterItems[1]).getByText('Group1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when adding a new filter item', () => {
|
||||
it('it should add the new item but not call onChange', async () => {
|
||||
const filters = {};
|
||||
const onChange = jest.fn();
|
||||
render(<MultiFilter filters={filters} onChange={onChange} />);
|
||||
|
||||
userEvent.click(screen.getByLabelText('Add'));
|
||||
expect(screen.getByTestId('cloudwatch-multifilter-item')).toBeInTheDocument();
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when adding a new filter item with key', () => {
|
||||
it('it should add the new item but not call onChange', async () => {
|
||||
const filters = {};
|
||||
const onChange = jest.fn();
|
||||
const { container } = render(<MultiFilter filters={filters} onChange={onChange} />);
|
||||
|
||||
userEvent.click(screen.getByLabelText('Add'));
|
||||
const filterItemElement = screen.getByTestId('cloudwatch-multifilter-item');
|
||||
expect(filterItemElement).toBeInTheDocument();
|
||||
|
||||
const keyElement = container.querySelector('#cloudwatch-multifilter-item-key');
|
||||
expect(keyElement).toBeInTheDocument();
|
||||
userEvent.type(keyElement!, 'my-key');
|
||||
fireEvent.keyDown(keyElement!, { keyCode: 13 });
|
||||
|
||||
expect(within(filterItemElement).getByDisplayValue('my-key')).toBeInTheDocument();
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when adding a new filter item with key and value', () => {
|
||||
it('it should add the new item and trigger onChange', async () => {
|
||||
const filters = {};
|
||||
const onChange = jest.fn();
|
||||
const { container } = render(<MultiFilter filters={filters} onChange={onChange} />);
|
||||
|
||||
const label = await screen.findByLabelText('Add');
|
||||
userEvent.click(label);
|
||||
const filterItemElement = screen.getByTestId('cloudwatch-multifilter-item');
|
||||
expect(filterItemElement).toBeInTheDocument();
|
||||
|
||||
const keyElement = container.querySelector('#cloudwatch-multifilter-item-key');
|
||||
expect(keyElement).toBeInTheDocument();
|
||||
userEvent.type(keyElement!, 'my-key');
|
||||
fireEvent.keyDown(keyElement!, { keyCode: 13 });
|
||||
expect(within(filterItemElement).getByDisplayValue('my-key')).toBeInTheDocument();
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
|
||||
const valueElement = container.querySelector('#cloudwatch-multifilter-item-value');
|
||||
expect(valueElement).toBeInTheDocument();
|
||||
userEvent.type(valueElement!, 'my-value');
|
||||
fireEvent.keyDown(valueElement!, { keyCode: 13 });
|
||||
expect(within(filterItemElement).getByText('my-value')).toBeInTheDocument();
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
'my-key': ['my-value'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { isEqual } from 'lodash';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EditorList } from '@grafana/experimental';
|
||||
import { MultiFilters } from '../../types';
|
||||
import { MultiFilterItem } from './MultiFilterItem';
|
||||
|
||||
export interface Props {
|
||||
filters?: MultiFilters;
|
||||
onChange: (filters: MultiFilters) => void;
|
||||
}
|
||||
|
||||
export interface MultiFilterCondition {
|
||||
key?: string;
|
||||
operator?: string;
|
||||
value?: string[];
|
||||
}
|
||||
|
||||
const multiFiltersToFilterConditions = (filters: MultiFilters | undefined) =>
|
||||
Object.entries(filters ?? {}).reduce<MultiFilterCondition[]>((acc, [key, value]) => {
|
||||
if (value && typeof value === 'object') {
|
||||
const filter = {
|
||||
key,
|
||||
value,
|
||||
operator: '=',
|
||||
};
|
||||
return [...acc, filter];
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const filterConditionsToMultiFilters = (filters: MultiFilterCondition[]) => {
|
||||
return filters.reduce<MultiFilters>((acc, { key, value }) => {
|
||||
if (key && value) {
|
||||
return { ...acc, [key]: value };
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const MultiFilter: React.FC<Props> = ({ filters, onChange }) => {
|
||||
const [items, setItems] = useState<MultiFilterCondition[]>([]);
|
||||
useEffect(() => setItems(multiFiltersToFilterConditions(filters)), [filters]);
|
||||
const onFiltersChange = (newItems: Array<Partial<MultiFilterCondition>>) => {
|
||||
setItems(newItems);
|
||||
|
||||
// The onChange event should only be triggered in the case there is a complete dimension object.
|
||||
// So when a new key is added that does not yet have a value, it should not trigger an onChange event.
|
||||
const newMultifilters = filterConditionsToMultiFilters(newItems);
|
||||
if (!isEqual(newMultifilters, filters)) {
|
||||
onChange(newMultifilters);
|
||||
}
|
||||
};
|
||||
|
||||
return <EditorList items={items} onChange={onFiltersChange} renderItem={renderFilter} />;
|
||||
};
|
||||
|
||||
function renderFilter(
|
||||
item: MultiFilterCondition,
|
||||
onChange: (item: MultiFilterCondition) => void,
|
||||
onDelete: () => void
|
||||
) {
|
||||
return <MultiFilterItem filter={item} onChange={(item) => onChange(item)} onDelete={onDelete} />;
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { GrafanaTheme2, toOption } from '@grafana/data';
|
||||
import { AccessoryButton, InputGroup } from '@grafana/experimental';
|
||||
import { Input, MultiSelect, stylesFactory, useTheme2 } from '@grafana/ui';
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { MultiFilterCondition } from './MultiFilter';
|
||||
|
||||
export interface Props {
|
||||
filter: MultiFilterCondition;
|
||||
onChange: (value: MultiFilterCondition) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export const MultiFilterItem: FunctionComponent<Props> = ({ filter, onChange, onDelete }) => {
|
||||
const [localKey, setLocalKey] = useState(filter.key || '');
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const theme = useTheme2();
|
||||
const styles = getOperatorStyles(theme);
|
||||
|
||||
const handleInputChange = (inputValue: string) => {
|
||||
setInputValue(inputValue);
|
||||
};
|
||||
const handleKeyDown = (keyEvent: React.KeyboardEvent) => {
|
||||
if (['Tab', 'Enter'].includes(keyEvent.key)) {
|
||||
onChange({ ...filter, value: filter.value ? [...filter.value, inputValue] : [inputValue] });
|
||||
setInputValue('');
|
||||
keyEvent.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="cloudwatch-multifilter-item">
|
||||
<InputGroup>
|
||||
<Input
|
||||
aria-label="Filter key"
|
||||
id="cloudwatch-multifilter-item-key"
|
||||
value={localKey}
|
||||
placeholder="key"
|
||||
onChange={(e) => setLocalKey(e.currentTarget.value)}
|
||||
onBlur={() => {
|
||||
if (localKey && localKey !== filter.key) {
|
||||
onChange({ key: localKey, value: undefined });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<span className={cx(styles.root)}>=</span>
|
||||
|
||||
<MultiSelect
|
||||
aria-label="Filter value"
|
||||
value={filter.value ? filter.value.map(toOption) : []}
|
||||
inputId="cloudwatch-multifilter-item-value"
|
||||
inputValue={inputValue}
|
||||
allowCustomValue
|
||||
onChange={(items) => {
|
||||
onChange({ ...filter, value: items.map((item) => item.value ?? '') });
|
||||
}}
|
||||
onInputChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
isOpen={false}
|
||||
hideIndicator
|
||||
options={filter.value ? filter.value.map(toOption) : []}
|
||||
placeholder="value"
|
||||
/>
|
||||
<AccessoryButton aria-label="remove" icon="times" variant="secondary" onClick={onDelete} type="button" />
|
||||
</InputGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getOperatorStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
||||
root: css({
|
||||
padding: theme.spacing(0, 1),
|
||||
alignSelf: 'center',
|
||||
}),
|
||||
}));
|
||||
+38
-4
@@ -1,4 +1,5 @@
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { select } from 'react-select-event';
|
||||
|
||||
@@ -13,11 +14,9 @@ const defaultQuery = {
|
||||
region: '',
|
||||
metricName: '',
|
||||
dimensionKey: '',
|
||||
ec2Filters: '',
|
||||
instanceID: '',
|
||||
attributeName: '',
|
||||
resourceType: '',
|
||||
tags: '',
|
||||
refId: '',
|
||||
};
|
||||
|
||||
@@ -40,7 +39,7 @@ ds.datasource.getMetrics = jest.fn().mockResolvedValue([
|
||||
]);
|
||||
ds.datasource.getDimensionKeys = jest
|
||||
.fn()
|
||||
.mockImplementation((namespace: string, region: string, dimensionFilters?: Dimensions) => {
|
||||
.mockImplementation((_namespace: string, region: string, dimensionFilters?: Dimensions) => {
|
||||
if (!!dimensionFilters) {
|
||||
return Promise.resolve([
|
||||
{ label: 's4', value: 's4' },
|
||||
@@ -61,6 +60,7 @@ ds.datasource.getDimensionValues = jest.fn().mockResolvedValue([
|
||||
{ label: 'bar', value: 'bar' },
|
||||
]);
|
||||
ds.datasource.getVariables = jest.fn().mockReturnValue([]);
|
||||
ds.datasource.getEc2InstanceAttribute = jest.fn().mockReturnValue([]);
|
||||
|
||||
const onChange = jest.fn();
|
||||
const defaultProps: Props = {
|
||||
@@ -167,6 +167,40 @@ describe('VariableEditor', () => {
|
||||
dimensionFilters: { v4: 'bar' },
|
||||
});
|
||||
});
|
||||
it('should parse multiFilters correctly', async () => {
|
||||
const props = defaultProps;
|
||||
props.query = {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.EC2InstanceAttributes,
|
||||
region: 'a1',
|
||||
attributeName: 'Tags.blah',
|
||||
ec2Filters: { s4: ['foo', 'bar'] },
|
||||
};
|
||||
render(<VariableQueryEditor {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Tags.blah')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const filterItem = screen.getByTestId('cloudwatch-multifilter-item');
|
||||
expect(filterItem).toBeInTheDocument();
|
||||
expect(within(filterItem).getByText('foo')).toBeInTheDocument();
|
||||
expect(within(filterItem).getByText('bar')).toBeInTheDocument();
|
||||
|
||||
// set filter value
|
||||
const valueElement = filterItem.querySelector('#cloudwatch-multifilter-item-value');
|
||||
expect(valueElement).toBeInTheDocument();
|
||||
userEvent.type(valueElement!, 'baz');
|
||||
fireEvent.keyDown(valueElement!, { keyCode: 13 });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.EC2InstanceAttributes,
|
||||
region: 'a1',
|
||||
attributeName: 'Tags.blah',
|
||||
ec2Filters: { s4: ['foo', 'bar', 'baz'] },
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('and a different region is selected', () => {
|
||||
it('should clear invalid fields', async () => {
|
||||
|
||||
+61
-16
@@ -1,6 +1,7 @@
|
||||
import { union } from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
import { QueryEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { QueryEditorProps, SelectableValue, toOption } from '@grafana/data';
|
||||
import { InlineField } from '@grafana/ui';
|
||||
|
||||
import { Dimensions } from '..';
|
||||
@@ -9,6 +10,7 @@ import { useDimensionKeys, useMetrics, useNamespaces, useRegions } from '../../h
|
||||
import { migrateVariableQuery } from '../../migrations';
|
||||
import { CloudWatchJsonData, CloudWatchQuery, VariableQuery, VariableQueryType } from '../../types';
|
||||
|
||||
import { MultiFilter } from './MultiFilter';
|
||||
import { VariableQueryField } from './VariableQueryField';
|
||||
import { VariableTextField } from './VariableTextField';
|
||||
|
||||
@@ -160,19 +162,29 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
)}
|
||||
{parsedQuery.queryType === VariableQueryType.EC2InstanceAttributes && (
|
||||
<>
|
||||
<VariableTextField
|
||||
<VariableQueryField
|
||||
value={parsedQuery.attributeName}
|
||||
placeholder="attribute name"
|
||||
onBlur={(value: string) => onQueryChange({ ...parsedQuery, attributeName: value })}
|
||||
options={
|
||||
parsedQuery.attributeName ? union(ec2Attributes, [toOption(parsedQuery.attributeName)]) : ec2Attributes
|
||||
}
|
||||
onChange={(value: string) => onQueryChange({ ...parsedQuery, attributeName: value })}
|
||||
label="Attribute Name"
|
||||
inputId={`variable-query-attribute-name-${query.refId}`}
|
||||
allowCustomValue
|
||||
tooltip='Create a "Tags.<name>" value to select a tag'
|
||||
/>
|
||||
<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 })}
|
||||
<InlineField
|
||||
label="Filters"
|
||||
/>
|
||||
labelWidth={20}
|
||||
tooltip='Pre-defined ec2:DescribeInstances filters and tags to filter the returned values on. Tags should be formatted "tag:<name>" '
|
||||
>
|
||||
<MultiFilter
|
||||
filters={parsedQuery.ec2Filters}
|
||||
onChange={(filters) => {
|
||||
onChange({ ...parsedQuery, ec2Filters: filters });
|
||||
}}
|
||||
/>
|
||||
</InlineField>
|
||||
</>
|
||||
)}
|
||||
{parsedQuery.queryType === VariableQueryType.ResourceArns && (
|
||||
@@ -183,14 +195,47 @@ export const VariableQueryEditor = ({ query, datasource, onChange }: Props) => {
|
||||
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"
|
||||
/>
|
||||
<InlineField label="Tags" labelWidth={20} tooltip="Tags to filter the returned values on.">
|
||||
<MultiFilter
|
||||
filters={parsedQuery.tags}
|
||||
onChange={(filters) => {
|
||||
onChange({ ...parsedQuery, tags: filters });
|
||||
}}
|
||||
/>
|
||||
</InlineField>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ec2Attributes = [
|
||||
'AmiLaunchIndex',
|
||||
'Architecture',
|
||||
'ClientToken',
|
||||
'EbsOptimized',
|
||||
'EnaSupport',
|
||||
'Hypervisor',
|
||||
'IamInstanceProfile',
|
||||
'ImageId',
|
||||
'InstanceId',
|
||||
'InstanceLifecycle',
|
||||
'InstanceType',
|
||||
'KernelId',
|
||||
'KeyName',
|
||||
'LaunchTime',
|
||||
'Platform',
|
||||
'PrivateDnsName',
|
||||
'PrivateIpAddress',
|
||||
'PublicDnsName',
|
||||
'PublicIpAddress',
|
||||
'RamdiskId',
|
||||
'RootDeviceName',
|
||||
'RootDeviceType',
|
||||
'SourceDestCheck',
|
||||
'SpotInstanceRequestId',
|
||||
'SriovNetSupport',
|
||||
'SubnetId',
|
||||
'VirtualizationType',
|
||||
'VpcId',
|
||||
].map(toOption);
|
||||
|
||||
+3
-1
@@ -15,6 +15,7 @@ interface VariableQueryFieldProps<T> {
|
||||
inputId?: string;
|
||||
allowCustomValue?: boolean;
|
||||
isLoading?: boolean;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export const VariableQueryField = <T extends string | VariableQueryType>({
|
||||
@@ -25,9 +26,10 @@ export const VariableQueryField = <T extends string | VariableQueryType>({
|
||||
allowCustomValue = false,
|
||||
isLoading = false,
|
||||
inputId = label,
|
||||
tooltip,
|
||||
}: VariableQueryFieldProps<T>) => {
|
||||
return (
|
||||
<InlineField label={label} labelWidth={LABEL_WIDTH} htmlFor={inputId}>
|
||||
<InlineField label={label} labelWidth={LABEL_WIDTH} htmlFor={inputId} tooltip={tooltip}>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
aria-label={label}
|
||||
|
||||
@@ -227,11 +227,22 @@ describe('migration', () => {
|
||||
});
|
||||
describe('when resource_arns query is used', () => {
|
||||
it('should parse the query', () => {
|
||||
const query = migrateVariableQuery('resource_arns(us-east-1,rds:db,{"environment":["$environment"]})');
|
||||
const query = migrateVariableQuery(
|
||||
'resource_arns(eu-west-1,elasticloadbalancing:loadbalancer,{"elasticbeanstalk:environment-name":["myApp-dev","myApp-prod"]})'
|
||||
);
|
||||
expect(query.queryType).toBe(VariableQueryType.ResourceArns);
|
||||
expect(query.region).toBe('eu-west-1');
|
||||
expect(query.resourceType).toBe('elasticloadbalancing:loadbalancer');
|
||||
expect(query.tags).toStrictEqual({ 'elasticbeanstalk:environment-name': ['myApp-dev', 'myApp-prod'] });
|
||||
});
|
||||
});
|
||||
describe('when ec2_instance_attribute query is used', () => {
|
||||
it('should parse the query', () => {
|
||||
const query = migrateVariableQuery('ec2_instance_attribute(us-east-1,rds:db,{"environment":["$environment"]})');
|
||||
expect(query.queryType).toBe(VariableQueryType.EC2InstanceAttributes);
|
||||
expect(query.region).toBe('us-east-1');
|
||||
expect(query.resourceType).toBe('rds:db');
|
||||
expect(query.tags).toBe('{"environment":["$environment"]}');
|
||||
expect(query.attributeName).toBe('rds:db');
|
||||
expect(query.ec2Filters).toStrictEqual({ environment: ['$environment'] });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,11 +82,11 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery): Variable
|
||||
metricName: '',
|
||||
dimensionKey: '',
|
||||
dimensionFilters: {},
|
||||
ec2Filters: '',
|
||||
ec2Filters: {},
|
||||
instanceID: '',
|
||||
attributeName: '',
|
||||
resourceType: '',
|
||||
tags: '',
|
||||
tags: {},
|
||||
};
|
||||
if (rawQuery === '') {
|
||||
return newQuery;
|
||||
@@ -147,7 +147,13 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery): Variable
|
||||
newQuery.queryType = VariableQueryType.EC2InstanceAttributes;
|
||||
newQuery.region = ec2InstanceAttributeQuery[1];
|
||||
newQuery.attributeName = ec2InstanceAttributeQuery[2];
|
||||
newQuery.ec2Filters = ec2InstanceAttributeQuery[3] || '';
|
||||
if (!!ec2InstanceAttributeQuery[3]) {
|
||||
try {
|
||||
newQuery.ec2Filters = JSON.parse(ec2InstanceAttributeQuery[3]);
|
||||
} catch {
|
||||
throw new Error(`unable to migrate poorly formed filters: ${ec2InstanceAttributeQuery[3]}`);
|
||||
}
|
||||
}
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
@@ -156,7 +162,13 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery): Variable
|
||||
newQuery.queryType = VariableQueryType.ResourceArns;
|
||||
newQuery.region = resourceARNsQuery[1];
|
||||
newQuery.resourceType = resourceARNsQuery[2];
|
||||
newQuery.tags = resourceARNsQuery[3] || '';
|
||||
if (!!resourceARNsQuery[3]) {
|
||||
try {
|
||||
newQuery.tags = JSON.parse(resourceARNsQuery[3]);
|
||||
} catch {
|
||||
throw new Error(`unable to migrate poorly formed filters: ${resourceARNsQuery[3]}`);
|
||||
}
|
||||
}
|
||||
return newQuery;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ export interface Dimensions {
|
||||
[key: string]: string | string[];
|
||||
}
|
||||
|
||||
export interface MultiFilters {
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
import {
|
||||
QueryEditorArrayExpression,
|
||||
QueryEditorFunctionExpression,
|
||||
@@ -394,9 +398,9 @@ export interface VariableQuery extends DataQuery {
|
||||
metricName: string;
|
||||
dimensionKey: string;
|
||||
dimensionFilters?: Dimensions;
|
||||
ec2Filters: string;
|
||||
ec2Filters?: MultiFilters;
|
||||
instanceID: string;
|
||||
attributeName: string;
|
||||
resourceType: string;
|
||||
tags: string;
|
||||
tags?: MultiFilters;
|
||||
}
|
||||
|
||||
@@ -8,11 +8,9 @@ const defaultQuery: VariableQuery = {
|
||||
region: 'bar',
|
||||
metricName: '',
|
||||
dimensionKey: '',
|
||||
ec2Filters: '',
|
||||
instanceID: '',
|
||||
attributeName: '',
|
||||
resourceType: '',
|
||||
tags: '',
|
||||
refId: '',
|
||||
};
|
||||
|
||||
@@ -114,7 +112,7 @@ describe('variables', () => {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.EC2InstanceAttributes,
|
||||
attributeName: 'abc',
|
||||
ec2Filters: '{"a":["b"]}',
|
||||
ec2Filters: { a: ['b'] },
|
||||
};
|
||||
beforeEach(() => {
|
||||
ds.datasource.getEc2InstanceAttribute = getEc2InstanceAttribute;
|
||||
@@ -139,7 +137,7 @@ describe('variables', () => {
|
||||
...defaultQuery,
|
||||
queryType: VariableQueryType.ResourceArns,
|
||||
resourceType: 'abc',
|
||||
tags: '{"a":["b"]}',
|
||||
tags: { a: ['b'] },
|
||||
};
|
||||
beforeEach(() => {
|
||||
ds.datasource.getResourceARNs = getResourceARNs;
|
||||
|
||||
@@ -122,11 +122,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
|
||||
if (!attributeName) {
|
||||
return [];
|
||||
}
|
||||
let filterJson = {};
|
||||
if (ec2Filters) {
|
||||
filterJson = JSON.parse(ec2Filters);
|
||||
}
|
||||
const values = await this.datasource.getEc2InstanceAttribute(region, attributeName, filterJson);
|
||||
const values = await this.datasource.getEc2InstanceAttribute(region, attributeName, ec2Filters ?? {});
|
||||
return values.map((s: { label: string; value: string }) => ({
|
||||
text: s.label,
|
||||
value: s.value,
|
||||
@@ -138,11 +134,7 @@ export class CloudWatchVariableSupport extends CustomVariableSupport<CloudWatchD
|
||||
if (!resourceType) {
|
||||
return [];
|
||||
}
|
||||
let tagJson = {};
|
||||
if (tags) {
|
||||
tagJson = JSON.parse(tags);
|
||||
}
|
||||
const keys = await this.datasource.getResourceARNs(region, resourceType, tagJson);
|
||||
const keys = await this.datasource.getResourceARNs(region, resourceType, tags ?? {});
|
||||
return keys.map((s: { label: string; value: string }) => ({
|
||||
text: s.label,
|
||||
value: s.value,
|
||||
|
||||
Reference in New Issue
Block a user