CloudWatch: Datasource improvements (#20268)

* CloudWatch: Datasource improvements

* Add statistic as template variale

* Add wildcard to list of values

* Template variable intercept dimension key

* Return row specific errors when transformation error occured

* Add meta feedback

* Make it possible to retrieve values without known metrics

* Add curated dashboard for EC2

* Fix broken tests

* Use correct dashboard name

* Display alert in case multi template var is being used for some certain props in the cloudwatch query

* Minor fixes after feedback

* Update dashboard json

* Update snapshot test

* Make sure region default is intercepted in cloudwatch link

* Update dashboards

* Include ec2 dashboard in ds

* Do not include ec2 dashboard in beta1

* Display actual region
This commit is contained in:
Erik Sundell
2019-11-14 10:59:41 +01:00
committed by GitHub
parent 1f018adbf3
commit 00bef917ee
62 changed files with 7552 additions and 1537 deletions

View File

@@ -0,0 +1,10 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { Alias } from './Alias';
describe('Alias', () => {
it('should render component', () => {
const tree = renderer.create(<Alias value={'legend'} onChange={() => {}} />).toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,21 @@
import React, { FunctionComponent, useState } from 'react';
import { debounce } from 'lodash';
import { Input } from '@grafana/ui';
export interface Props {
onChange: (alias: any) => void;
value: string;
}
export const Alias: FunctionComponent<Props> = ({ value = '', onChange }) => {
const [alias, setAlias] = useState(value);
const propagateOnChange = debounce(onChange, 1500);
onChange = (e: any) => {
setAlias(e.target.value);
propagateOnChange(e.target.value);
};
return <Input type="text" className="gf-form-input width-16" value={alias} onChange={onChange} />;
};

View File

@@ -0,0 +1,106 @@
import React from 'react';
import { shallow } from 'enzyme';
import ConfigEditor, { Props } from './ConfigEditor';
jest.mock('app/features/plugins/datasource_srv', () => ({
getDatasourceSrv: () => ({
loadDatasource: jest.fn().mockImplementation(() =>
Promise.resolve({
getRegions: jest.fn().mockReturnValue([
{
label: 'ap-east-1',
value: 'ap-east-1',
},
]),
})
),
}),
}));
const setup = (propOverrides?: object) => {
const props: Props = {
options: {
id: 1,
orgId: 1,
typeLogoUrl: '',
name: 'CloudWatch',
access: 'proxy',
url: '',
database: '',
type: 'cloudwatch',
user: '',
password: '',
basicAuth: false,
basicAuthPassword: '',
basicAuthUser: '',
isDefault: true,
readOnly: false,
withCredentials: false,
secureJsonFields: {
accessKey: false,
secretKey: false,
},
jsonData: {
assumeRoleArn: '',
database: '',
customMetricsNamespaces: '',
authType: 'keys',
defaultRegion: 'us-east-2',
timeField: '@timestamp',
},
secureJsonData: {
secretKey: '',
accessKey: '',
},
},
onOptionsChange: jest.fn(),
};
Object.assign(props, propOverrides);
return shallow(<ConfigEditor {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
it('should disable access key id field', () => {
const wrapper = setup({
secureJsonFields: {
secretKey: true,
},
});
expect(wrapper).toMatchSnapshot();
});
it('should should show credentials profile name field', () => {
const wrapper = setup({
jsonData: {
authType: 'credentials',
},
});
expect(wrapper).toMatchSnapshot();
});
it('should should show access key and secret access key fields', () => {
const wrapper = setup({
jsonData: {
authType: 'keys',
},
});
expect(wrapper).toMatchSnapshot();
});
it('should should show arn role field', () => {
const wrapper = setup({
jsonData: {
authType: 'arn',
},
});
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,390 @@
import React, { PureComponent, ChangeEvent } from 'react';
import { FormLabel, Select, Input, Button } from '@grafana/ui';
import { DataSourcePluginOptionsEditorProps, DataSourceSettings } from '@grafana/data';
import { SelectableValue } from '@grafana/data';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import CloudWatchDatasource from '../datasource';
import { CloudWatchJsonData, CloudWatchSecureJsonData } from '../types';
export type Props = DataSourcePluginOptionsEditorProps<CloudWatchJsonData>;
type CloudwatchSettings = DataSourceSettings<CloudWatchJsonData, CloudWatchSecureJsonData>;
export interface State {
config: CloudwatchSettings;
authProviderOptions: SelectableValue[];
regions: SelectableValue[];
}
export class ConfigEditor extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const { options } = this.props;
this.state = {
config: ConfigEditor.defaults(options),
authProviderOptions: [
{ label: 'Access & secret key', value: 'keys' },
{ label: 'Credentials file', value: 'credentials' },
{ label: 'ARN', value: 'arn' },
],
regions: [],
};
this.updateDatasource(this.state.config);
}
static getDerivedStateFromProps(props: Props, state: State) {
return {
...state,
config: ConfigEditor.defaults(props.options),
};
}
static defaults = (options: any) => {
options.jsonData.authType = options.jsonData.authType || 'credentials';
options.jsonData.timeField = options.jsonData.timeField || '@timestamp';
if (!options.hasOwnProperty('secureJsonData')) {
options.secureJsonData = {};
}
if (!options.hasOwnProperty('jsonData')) {
options.jsonData = {};
}
if (!options.hasOwnProperty('secureJsonFields')) {
options.secureJsonFields = {};
}
return options;
};
async componentDidMount() {
this.loadRegions();
}
loadRegions() {
getDatasourceSrv()
.loadDatasource(this.state.config.name)
.then((ds: CloudWatchDatasource) => {
return ds.getRegions();
})
.then(
(regions: any) => {
this.setState({
regions: regions.map((region: any) => {
return {
value: region.value,
label: region.text,
};
}),
});
},
(err: any) => {
const regions = [
'ap-east-1',
'ap-northeast-1',
'ap-northeast-2',
'ap-northeast-3',
'ap-south-1',
'ap-southeast-1',
'ap-southeast-2',
'ca-central-1',
'cn-north-1',
'cn-northwest-1',
'eu-central-1',
'eu-north-1',
'eu-west-1',
'eu-west-2',
'eu-west-3',
'me-south-1',
'sa-east-1',
'us-east-1',
'us-east-2',
'us-gov-east-1',
'us-gov-west-1',
'us-iso-east-1',
'us-isob-east-1',
'us-west-1',
'us-west-2',
];
this.setState({
regions: regions.map((region: string) => {
return {
value: region,
label: region,
};
}),
});
// expected to fail when creating new datasource
// console.error('failed to get latest regions', err);
}
);
}
updateDatasource = async (config: any) => {
for (const j in config.jsonData) {
if (config.jsonData[j].length === 0) {
delete config.jsonData[j];
}
}
for (const k in config.secureJsonData) {
if (config.secureJsonData[k].length === 0) {
delete config.secureJsonData[k];
}
}
this.props.onOptionsChange({
...config,
});
};
onAuthProviderChange = (authType: SelectableValue<string>) => {
this.updateDatasource({
...this.state.config,
jsonData: {
...this.state.config.jsonData,
authType: authType.value,
},
});
};
onRegionChange = (defaultRegion: SelectableValue<string>) => {
this.updateDatasource({
...this.state.config,
jsonData: {
...this.state.config.jsonData,
defaultRegion: defaultRegion.value,
},
});
};
onResetAccessKey = () => {
this.updateDatasource({
...this.state.config,
secureJsonFields: {
...this.state.config.secureJsonFields,
accessKey: false,
},
});
};
onAccessKeyChange = (accessKey: string) => {
this.updateDatasource({
...this.state.config,
secureJsonData: {
...this.state.config.secureJsonData,
accessKey,
},
});
};
onResetSecretKey = () => {
this.updateDatasource({
...this.state.config,
secureJsonFields: {
...this.state.config.secureJsonFields,
secretKey: false,
},
});
};
onSecretKeyChange = (secretKey: string) => {
this.updateDatasource({
...this.state.config,
secureJsonData: {
...this.state.config.secureJsonData,
secretKey,
},
});
};
onCredentialProfileNameChange = (database: string) => {
this.updateDatasource({
...this.state.config,
database,
});
};
onArnAssumeRoleChange = (assumeRoleArn: string) => {
this.updateDatasource({
...this.state.config,
jsonData: {
...this.state.config.jsonData,
assumeRoleArn,
},
});
};
onCustomMetricsNamespacesChange = (customMetricsNamespaces: string) => {
this.updateDatasource({
...this.state.config,
jsonData: {
...this.state.config.jsonData,
customMetricsNamespaces,
},
});
};
render() {
const { config, authProviderOptions, regions } = this.state;
return (
<>
<h3 className="page-heading">CloudWatch Details</h3>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form">
<FormLabel className="width-14">Auth Provider</FormLabel>
<Select
className="width-30"
value={authProviderOptions.find(authProvider => authProvider.value === config.jsonData.authType)}
options={authProviderOptions}
defaultValue={config.jsonData.authType}
onChange={this.onAuthProviderChange}
/>
</div>
</div>
{config.jsonData.authType === 'credentials' && (
<div className="gf-form-inline">
<div className="gf-form">
<FormLabel
className="width-14"
tooltip="Credentials profile name, as specified in ~/.aws/credentials, leave blank for default."
>
Credentials Profile Name
</FormLabel>
<div className="width-30">
<Input
className="width-30"
placeholder="default"
value={config.jsonData.database}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
this.onCredentialProfileNameChange(event.target.value)
}
/>
</div>
</div>
</div>
)}
{config.jsonData.authType === 'keys' && (
<div>
{config.secureJsonFields.accessKey ? (
<div className="gf-form-inline">
<div className="gf-form">
<FormLabel className="width-14">Access Key ID</FormLabel>
<Input className="width-25" placeholder="Configured" disabled={true} />
</div>
<div className="gf-form">
<div className="max-width-30 gf-form-inline">
<Button variant="secondary" type="button" onClick={this.onResetAccessKey}>
Reset
</Button>
</div>
</div>
</div>
) : (
<div className="gf-form-inline">
<div className="gf-form">
<FormLabel className="width-14">Access Key ID</FormLabel>
<div className="width-30">
<Input
className="width-30"
value={config.secureJsonData.accessKey || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => this.onAccessKeyChange(event.target.value)}
/>
</div>
</div>
</div>
)}
{config.secureJsonFields.secretKey ? (
<div className="gf-form-inline">
<div className="gf-form">
<FormLabel className="width-14">Secret Access Key</FormLabel>
<Input className="width-25" placeholder="Configured" disabled={true} />
</div>
<div className="gf-form">
<div className="max-width-30 gf-form-inline">
<Button variant="secondary" type="button" onClick={this.onResetSecretKey}>
Reset
</Button>
</div>
</div>
</div>
) : (
<div className="gf-form-inline">
<div className="gf-form">
<FormLabel className="width-14">Secret Access Key</FormLabel>
<div className="width-30">
<Input
className="width-30"
value={config.secureJsonData.secretKey || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => this.onSecretKeyChange(event.target.value)}
/>
</div>
</div>
</div>
)}
</div>
)}
{config.jsonData.authType === 'arn' && (
<div className="gf-form-inline">
<div className="gf-form">
<FormLabel className="width-14" tooltip="ARN of Assume Role">
Assume Role ARN
</FormLabel>
<div className="width-30">
<Input
className="width-30"
placeholder="arn:aws:iam:*"
value={config.jsonData.assumeRoleArn || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => this.onArnAssumeRoleChange(event.target.value)}
/>
</div>
</div>
</div>
)}
<div className="gf-form-inline">
<div className="gf-form">
<FormLabel
className="width-14"
tooltip="Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region."
>
Default Region
</FormLabel>
<Select
className="width-30"
value={regions.find(region => region.value === config.jsonData.defaultRegion)}
options={regions}
defaultValue={config.jsonData.defaultRegion}
onChange={this.onRegionChange}
/>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<FormLabel className="width-14" tooltip="Namespaces of Custom Metrics.">
Custom Metrics
</FormLabel>
<Input
className="width-30"
placeholder="Namespace1,Namespace2"
value={config.jsonData.customMetricsNamespaces || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
this.onCustomMetricsNamespacesChange(event.target.value)
}
/>
</div>
</div>
</div>
</>
);
}
}
export default ConfigEditor;

View File

@@ -0,0 +1,50 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { Dimensions } from './';
import { SelectableStrings } from '../types';
describe('Dimensions', () => {
it('renders', () => {
mount(
<Dimensions
dimensions={{}}
onChange={dimensions => console.log(dimensions)}
loadKeys={() => Promise.resolve<SelectableStrings>([])}
loadValues={() => Promise.resolve<SelectableStrings>([])}
/>
);
});
describe('and no dimension were passed to the component', () => {
it('initially displays just an add button', () => {
const wrapper = shallow(
<Dimensions
dimensions={{}}
onChange={() => {}}
loadKeys={() => Promise.resolve<SelectableStrings>([])}
loadValues={() => Promise.resolve<SelectableStrings>([])}
/>
);
expect(wrapper.html()).toEqual(
`<div class="gf-form"><a class="gf-form-label query-part"><i class="fa fa-plus"></i></a></div>`
);
});
});
describe('and one dimension key along with a value were passed to the component', () => {
it('initially displays the dimension key, value and an add button', () => {
const wrapper = shallow(
<Dimensions
dimensions={{ somekey: 'somevalue' }}
onChange={() => {}}
loadKeys={() => Promise.resolve<SelectableStrings>([])}
loadValues={() => Promise.resolve<SelectableStrings>([])}
/>
);
expect(wrapper.html()).toEqual(
`<div class="gf-form"><a class="gf-form-label query-part">somekey</a></div><label class="gf-form-label query-segment-operator">=</label><div class="gf-form"><a class="gf-form-label query-part">somevalue</a></div><div class="gf-form"><a class="gf-form-label query-part"><i class="fa fa-plus"></i></a></div>`
);
});
});
});

View File

@@ -0,0 +1,80 @@
import React, { FunctionComponent, Fragment, useState, useEffect } from 'react';
import isEqual from 'lodash/isEqual';
import { SelectableValue } from '@grafana/data';
import { SegmentAsync } from '@grafana/ui';
import { SelectableStrings } from '../types';
export interface Props {
dimensions: { [key: string]: string | string[] };
onChange: (dimensions: { [key: string]: string }) => void;
loadValues: (key: string) => Promise<SelectableStrings>;
loadKeys: () => Promise<SelectableStrings>;
}
const removeText = '-- remove dimension --';
const removeOption: SelectableValue<string> = { label: removeText, value: removeText };
// The idea of this component is that is should only trigger the onChange event in the case
// there is a complete dimension object. E.g, when a new key is added is doesn't have a value.
// That should not trigger onChange.
export const Dimensions: FunctionComponent<Props> = ({ dimensions, loadValues, loadKeys, onChange }) => {
const [data, setData] = useState(dimensions);
useEffect(() => {
const completeDimensions = Object.entries(data).reduce(
(res, [key, value]) => (value ? { ...res, [key]: value } : res),
{}
);
if (!isEqual(completeDimensions, dimensions)) {
onChange(completeDimensions);
}
}, [data]);
const excludeUsedKeys = (options: SelectableStrings) => {
return options.filter(({ value }) => !Object.keys(data).includes(value));
};
return (
<>
{Object.entries(data).map(([key, value], index) => (
<Fragment key={index}>
<SegmentAsync
allowCustomValue
value={key}
loadOptions={() => loadKeys().then(keys => [removeOption, ...excludeUsedKeys(keys)])}
onChange={newKey => {
const { [key]: value, ...newDimensions } = data;
if (newKey === removeText) {
setData({ ...newDimensions });
} else {
setData({ ...newDimensions, [newKey]: '' });
}
}}
/>
<label className="gf-form-label query-segment-operator">=</label>
<SegmentAsync
allowCustomValue
value={value || 'select dimension value'}
loadOptions={() => loadValues(key)}
onChange={newValue => setData({ ...data, [key]: newValue })}
/>
{Object.values(data).length > 1 && index + 1 !== Object.values(data).length && (
<label className="gf-form-label query-keyword">AND</label>
)}
</Fragment>
))}
{Object.values(data).every(v => v) && (
<SegmentAsync
allowCustomValue
Component={
<a className="gf-form-label query-part">
<i className="fa fa-plus" />
</a>
}
loadOptions={() => loadKeys().then(excludeUsedKeys)}
onChange={(newKey: string) => setData({ ...data, [newKey]: '' })}
/>
)}
</>
);
};

View File

@@ -0,0 +1,28 @@
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
import { FormLabel } from '@grafana/ui';
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
label: string;
tooltip?: string;
children?: React.ReactNode;
}
export const QueryField: FunctionComponent<Partial<Props>> = ({ label, tooltip, children }) => (
<>
<FormLabel width={8} className="query-keyword" tooltip={tooltip}>
{label}
</FormLabel>
{children}
</>
);
export const QueryInlineField: FunctionComponent<Props> = ({ ...props }) => {
return (
<div className={'gf-form-inline'}>
<QueryField {...props} />
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
);
};

View File

@@ -0,0 +1,102 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { mount } from 'enzyme';
import { DataSourceInstanceSettings } from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariable } from 'app/features/templating/all';
import { QueryEditor, Props } from './QueryEditor';
import CloudWatchDatasource from '../datasource';
const setup = () => {
const instanceSettings = {
jsonData: { defaultRegion: 'us-east-1' },
} as DataSourceInstanceSettings;
const templateSrv = new TemplateSrv();
templateSrv.init([
new CustomVariable(
{
name: 'var3',
options: [
{ selected: true, value: 'var3-foo' },
{ selected: false, value: 'var3-bar' },
{ selected: true, value: 'var3-baz' },
],
current: {
value: ['var3-foo', 'var3-baz'],
},
multi: true,
},
{} as any
),
]);
const datasource = new CloudWatchDatasource(instanceSettings, {} as any, {} as any, templateSrv as any, {} as any);
datasource.metricFindQuery = async param => [{ value: 'test', label: 'test' }];
const props: Props = {
query: {
refId: '',
id: '',
region: 'us-east-1',
namespace: 'ec2',
metricName: 'CPUUtilization',
dimensions: { somekey: 'somevalue' },
statistics: new Array<string>(),
period: '',
expression: '',
alias: '',
highResolution: false,
matchExact: true,
},
datasource,
onChange: jest.fn(),
onRunQuery: jest.fn(),
};
return props;
};
describe('QueryEditor', () => {
it('should render component', () => {
const props = setup();
const tree = renderer.create(<QueryEditor {...props} />).toJSON();
expect(tree).toMatchSnapshot();
});
describe('should use correct default values', () => {
it('when region is null is display default in the label', () => {
const props = setup();
props.query.region = null;
const wrapper = mount(<QueryEditor {...props} />);
expect(
wrapper
.find('.gf-form-inline')
.first()
.find('.gf-form-label.query-part')
.first()
.text()
).toEqual('default');
});
it('should init props correctly', () => {
const props = setup();
props.query.namespace = null;
props.query.metricName = null;
props.query.expression = null;
props.query.dimensions = null;
props.query.region = null;
props.query.statistics = null;
const wrapper = mount(<QueryEditor {...props} />);
const {
query: { namespace, region, metricName, dimensions, statistics, expression },
} = wrapper.props();
expect(namespace).toEqual('');
expect(metricName).toEqual('');
expect(expression).toEqual('');
expect(region).toEqual('default');
expect(statistics).toEqual(['Average']);
expect(dimensions).toEqual({});
});
});
});

View File

@@ -0,0 +1,277 @@
import React, { PureComponent, ChangeEvent } from 'react';
import { SelectableValue, QueryEditorProps } from '@grafana/data';
import { Input, Segment, SegmentAsync, ValidationEvents, EventsWithValidation, Switch } from '@grafana/ui';
import { CloudWatchQuery } from '../types';
import CloudWatchDatasource from '../datasource';
import { SelectableStrings } from '../types';
import { Stats, Dimensions, QueryInlineField, QueryField, Alias } from './';
export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery>;
interface State {
regions: SelectableStrings;
namespaces: SelectableStrings;
metricNames: SelectableStrings;
variableOptionGroup: SelectableValue<string>;
showMeta: boolean;
}
const idValidationEvents: ValidationEvents = {
[EventsWithValidation.onBlur]: [
{
rule: value => new RegExp(/^$|^[a-z][a-zA-Z0-9_]*$/).test(value),
errorMessage: 'Invalid format. Only alphanumeric characters and underscores are allowed',
},
],
};
export class QueryEditor extends PureComponent<Props, State> {
state: State = { regions: [], namespaces: [], metricNames: [], variableOptionGroup: {}, showMeta: false };
componentWillMount() {
const { query } = this.props;
if (!query.namespace) {
query.namespace = '';
}
if (!query.metricName) {
query.metricName = '';
}
if (!query.expression) {
query.expression = '';
}
if (!query.dimensions) {
query.dimensions = {};
}
if (!query.region) {
query.region = 'default';
}
if (!query.statistics || !query.statistics.length) {
query.statistics = ['Average'];
}
if (!query.hasOwnProperty('highResolution')) {
query.highResolution = false;
}
if (!query.hasOwnProperty('matchExact')) {
query.matchExact = true;
}
}
componentDidMount() {
const { datasource } = this.props;
const variableOptionGroup = {
label: 'Template Variables',
options: this.props.datasource.variables.map(this.toOption),
};
Promise.all([datasource.metricFindQuery('regions()'), datasource.metricFindQuery('namespaces()')]).then(
([regions, namespaces]) => {
this.setState({
...this.state,
regions: [...regions, variableOptionGroup],
namespaces: [...namespaces, variableOptionGroup],
variableOptionGroup,
});
}
);
}
loadMetricNames = async () => {
const { namespace, region } = this.props.query;
return this.props.datasource.metricFindQuery(`metrics(${namespace},${region})`).then(this.appendTemplateVariables);
};
appendTemplateVariables = (values: SelectableValue[]) => [
...values,
{ label: 'Template Variables', options: this.props.datasource.variables.map(this.toOption) },
];
toOption = (value: any) => ({ label: value, value });
onChange(query: CloudWatchQuery) {
const { onChange, onRunQuery } = this.props;
onChange(query);
onRunQuery();
}
render() {
const { query, datasource, onChange, onRunQuery, data } = this.props;
const { regions, namespaces, variableOptionGroup: variableOptionGroup, showMeta } = this.state;
const metaDataExist = data && Object.values(data).length && data.state === 'Done';
return (
<>
<QueryInlineField label="Region">
<Segment
value={query.region || 'Select region'}
options={regions}
allowCustomValue
onChange={region => this.onChange({ ...query, region })}
/>
</QueryInlineField>
{query.expression.length === 0 && (
<>
<QueryInlineField label="Namespace">
<Segment
value={query.namespace || 'Select namespace'}
allowCustomValue
options={namespaces}
onChange={namespace => this.onChange({ ...query, namespace })}
/>
</QueryInlineField>
<QueryInlineField label="Metric Name">
<SegmentAsync
value={query.metricName || 'Select metric name'}
allowCustomValue
loadOptions={this.loadMetricNames}
onChange={metricName => this.onChange({ ...query, metricName })}
/>
</QueryInlineField>
<QueryInlineField label="Stats">
<Stats
stats={datasource.standardStatistics.map(this.toOption)}
values={query.statistics}
onChange={statistics => this.onChange({ ...query, statistics })}
variableOptionGroup={variableOptionGroup}
/>
</QueryInlineField>
<QueryInlineField label="Dimensions">
<Dimensions
dimensions={query.dimensions}
onChange={dimensions => this.onChange({ ...query, dimensions })}
loadKeys={() =>
datasource.getDimensionKeys(query.namespace, query.region).then(this.appendTemplateVariables)
}
loadValues={newKey => {
const { [newKey]: value, ...newDimensions } = query.dimensions;
return datasource
.getDimensionValues(query.region, query.namespace, query.metricName, newKey, newDimensions)
.then(this.appendTemplateVariables);
}}
/>
</QueryInlineField>
</>
)}
{query.statistics.length <= 1 && (
<div className="gf-form-inline">
<div className="gf-form">
<QueryField
className="query-keyword"
label="Id"
tooltip="Id can include numbers, letters, and underscore, and must start with a lowercase letter."
>
<Input
className="gf-form-input width-8"
onBlur={onRunQuery}
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange({ ...query, id: event.target.value })}
validationEvents={idValidationEvents}
value={query.id || ''}
/>
</QueryField>
</div>
<div className="gf-form gf-form--grow">
<QueryField
className="gf-form--grow"
label="Expression"
tooltip="Optionally you can add an expression here. Please note that if a math expression that is referencing other queries is being used, it will not be possible to create an alert rule based on this query"
>
<Input
className="gf-form-input"
onBlur={onRunQuery}
value={query.expression || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
onChange({ ...query, expression: event.target.value })
}
/>
</QueryField>
</div>
</div>
)}
<div className="gf-form-inline">
<div className="gf-form">
<QueryField
className="query-keyword"
label="Min Period"
tooltip="Minimum interval between points in seconds"
>
<Input
className="gf-form-input width-8"
value={query.period || ''}
placeholder="auto"
onBlur={onRunQuery}
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange({ ...query, period: event.target.value })}
/>
</QueryField>
</div>
<div className="gf-form">
<QueryField
className="query-keyword"
label="Alias"
tooltip="Alias replacement variables: {{metric}}, {{stat}}, {{namespace}}, {{region}}, {{period}}, {{label}}, {{YOUR_DIMENSION_NAME}}"
>
<Alias value={query.alias} onChange={(value: string) => this.onChange({ ...query, alias: value })} />
</QueryField>
<Switch
label="HighRes"
labelClass="query-keyword"
checked={query.highResolution}
onChange={() => this.onChange({ ...query, highResolution: !query.highResolution })}
/>
<Switch
label="Match Exact"
labelClass="query-keyword"
tooltip="Only show metrics that exactly match all defined dimension names."
checked={query.matchExact}
onChange={() => this.onChange({ ...query, matchExact: !query.matchExact })}
/>
<label className="gf-form-label">
<a
onClick={() =>
metaDataExist &&
this.setState({
...this.state,
showMeta: !showMeta,
})
}
>
<i className={`fa fa-caret-${showMeta ? 'down' : 'right'}`} /> {showMeta ? 'Hide' : 'Show'} Query
Preview
</a>
</label>
</div>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
{showMeta && metaDataExist && (
<table className="filter-table form-inline">
<thead>
<tr>
<th>Metric Data Query ID</th>
<th>Metric Data Query Expression</th>
<th />
</tr>
</thead>
<tbody>
{data.series[0].meta.gmdMeta.map(({ ID, Expression }: any) => (
<tr key={ID}>
<td>{ID}</td>
<td>{Expression}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</>
);
}
}

View File

@@ -0,0 +1,21 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { Stats } from './Stats';
const toOption = (value: any) => ({ label: value, value });
describe('Stats', () => {
it('should render component', () => {
const tree = renderer
.create(
<Stats
values={['Average', 'Minimum']}
variableOptionGroup={{ label: 'templateVar', value: 'templateVar' }}
onChange={() => {}}
stats={['Average', 'Maximum', 'Minimum', 'Sum', 'SampleCount'].map(toOption)}
/>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,47 @@
import React, { FunctionComponent } from 'react';
import { SelectableStrings } from '../types';
import { SelectableValue } from '@grafana/data';
import { Segment } from '@grafana/ui';
export interface Props {
values: string[];
onChange: (values: string[]) => void;
variableOptionGroup: SelectableValue<string>;
stats: SelectableStrings;
}
const removeText = '-- remove stat --';
const removeOption: SelectableValue<string> = { label: removeText, value: removeText };
export const Stats: FunctionComponent<Props> = ({ stats, values, onChange, variableOptionGroup }) => (
<>
{values &&
values.map((value, index) => (
<Segment
allowCustomValue
key={value + index}
value={value}
options={[removeOption, ...stats, variableOptionGroup]}
onChange={value =>
onChange(
value === removeText
? values.filter((_, i) => i !== index)
: values.map((v, i) => (i === index ? value : v))
)
}
/>
))}
{values.length !== stats.length && (
<Segment
Component={
<a className="gf-form-label query-part">
<i className="fa fa-plus" />
</a>
}
allowCustomValue
onChange={(value: string) => onChange([...values, value])}
options={[...stats.filter(({ value }) => !values.includes(value)), variableOptionGroup]}
/>
)}
</>
);

View File

@@ -0,0 +1,27 @@
import React, { FunctionComponent } from 'react';
export interface Props {
region: string;
}
export const ThrottlingErrorMessage: FunctionComponent<Props> = ({ region }) => (
<p>
Please visit the&nbsp;
<a
target="_blank"
className="text-link"
href={`https://${region}.console.aws.amazon.com/servicequotas/home?region=${region}#!/services/monitoring/quotas/L-5E141212`}
>
AWS Service Quotas console
</a>
&nbsp;to request a quota increase or see our&nbsp;
<a
target="_blank"
className="text-link"
href={`https://grafana.com/docs/features/datasources/cloudwatch/#service-quotas`}
>
documentation
</a>
&nbsp;to learn more.
</p>
);

View File

@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Alias should render component 1`] = `
<div
style={
Object {
"flexGrow": 1,
}
}
>
<input
className="gf-form-input gf-form-input width-16"
onChange={[Function]}
type="text"
value="legend"
/>
</div>
`;

View File

@@ -0,0 +1,901 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should disable access key id field 1`] = `
<Fragment>
<h3
className="page-heading"
>
CloudWatch Details
</h3>
<div
className="gf-form-group"
>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Auth Provider
</Component>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue="keys"
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"label": "Access & secret key",
"value": "keys",
},
Object {
"label": "Credentials file",
"value": "credentials",
},
Object {
"label": "ARN",
"value": "arn",
},
]
}
tabSelectsValue={true}
value={
Object {
"label": "Access & secret key",
"value": "keys",
}
}
/>
</div>
</div>
<div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Access Key ID
</Component>
<div
className="width-30"
>
<Input
className="width-30"
onChange={[Function]}
value=""
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Secret Access Key
</Component>
<div
className="width-30"
>
<Input
className="width-30"
onChange={[Function]}
value=""
/>
</div>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
tooltip="Specify the region, such as for US West (Oregon) use \` us-west-2 \` as the region."
>
Default Region
</Component>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue="us-east-2"
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={Array []}
tabSelectsValue={true}
/>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
tooltip="Namespaces of Custom Metrics."
>
Custom Metrics
</Component>
<Input
className="width-30"
onChange={[Function]}
placeholder="Namespace1,Namespace2"
value=""
/>
</div>
</div>
</div>
</Fragment>
`;
exports[`Render should render component 1`] = `
<Fragment>
<h3
className="page-heading"
>
CloudWatch Details
</h3>
<div
className="gf-form-group"
>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Auth Provider
</Component>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue="keys"
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"label": "Access & secret key",
"value": "keys",
},
Object {
"label": "Credentials file",
"value": "credentials",
},
Object {
"label": "ARN",
"value": "arn",
},
]
}
tabSelectsValue={true}
value={
Object {
"label": "Access & secret key",
"value": "keys",
}
}
/>
</div>
</div>
<div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Access Key ID
</Component>
<div
className="width-30"
>
<Input
className="width-30"
onChange={[Function]}
value=""
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Secret Access Key
</Component>
<div
className="width-30"
>
<Input
className="width-30"
onChange={[Function]}
value=""
/>
</div>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
tooltip="Specify the region, such as for US West (Oregon) use \` us-west-2 \` as the region."
>
Default Region
</Component>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue="us-east-2"
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={Array []}
tabSelectsValue={true}
/>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
tooltip="Namespaces of Custom Metrics."
>
Custom Metrics
</Component>
<Input
className="width-30"
onChange={[Function]}
placeholder="Namespace1,Namespace2"
value=""
/>
</div>
</div>
</div>
</Fragment>
`;
exports[`Render should should show access key and secret access key fields 1`] = `
<Fragment>
<h3
className="page-heading"
>
CloudWatch Details
</h3>
<div
className="gf-form-group"
>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Auth Provider
</Component>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue="keys"
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"label": "Access & secret key",
"value": "keys",
},
Object {
"label": "Credentials file",
"value": "credentials",
},
Object {
"label": "ARN",
"value": "arn",
},
]
}
tabSelectsValue={true}
value={
Object {
"label": "Access & secret key",
"value": "keys",
}
}
/>
</div>
</div>
<div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Access Key ID
</Component>
<div
className="width-30"
>
<Input
className="width-30"
onChange={[Function]}
value=""
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Secret Access Key
</Component>
<div
className="width-30"
>
<Input
className="width-30"
onChange={[Function]}
value=""
/>
</div>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
tooltip="Specify the region, such as for US West (Oregon) use \` us-west-2 \` as the region."
>
Default Region
</Component>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue="us-east-2"
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={Array []}
tabSelectsValue={true}
/>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
tooltip="Namespaces of Custom Metrics."
>
Custom Metrics
</Component>
<Input
className="width-30"
onChange={[Function]}
placeholder="Namespace1,Namespace2"
value=""
/>
</div>
</div>
</div>
</Fragment>
`;
exports[`Render should should show arn role field 1`] = `
<Fragment>
<h3
className="page-heading"
>
CloudWatch Details
</h3>
<div
className="gf-form-group"
>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Auth Provider
</Component>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue="keys"
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"label": "Access & secret key",
"value": "keys",
},
Object {
"label": "Credentials file",
"value": "credentials",
},
Object {
"label": "ARN",
"value": "arn",
},
]
}
tabSelectsValue={true}
value={
Object {
"label": "Access & secret key",
"value": "keys",
}
}
/>
</div>
</div>
<div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Access Key ID
</Component>
<div
className="width-30"
>
<Input
className="width-30"
onChange={[Function]}
value=""
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Secret Access Key
</Component>
<div
className="width-30"
>
<Input
className="width-30"
onChange={[Function]}
value=""
/>
</div>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
tooltip="Specify the region, such as for US West (Oregon) use \` us-west-2 \` as the region."
>
Default Region
</Component>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue="us-east-2"
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={Array []}
tabSelectsValue={true}
/>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
tooltip="Namespaces of Custom Metrics."
>
Custom Metrics
</Component>
<Input
className="width-30"
onChange={[Function]}
placeholder="Namespace1,Namespace2"
value=""
/>
</div>
</div>
</div>
</Fragment>
`;
exports[`Render should should show credentials profile name field 1`] = `
<Fragment>
<h3
className="page-heading"
>
CloudWatch Details
</h3>
<div
className="gf-form-group"
>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Auth Provider
</Component>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue="keys"
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={
Array [
Object {
"label": "Access & secret key",
"value": "keys",
},
Object {
"label": "Credentials file",
"value": "credentials",
},
Object {
"label": "ARN",
"value": "arn",
},
]
}
tabSelectsValue={true}
value={
Object {
"label": "Access & secret key",
"value": "keys",
}
}
/>
</div>
</div>
<div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Access Key ID
</Component>
<div
className="width-30"
>
<Input
className="width-30"
onChange={[Function]}
value=""
/>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
>
Secret Access Key
</Component>
<div
className="width-30"
>
<Input
className="width-30"
onChange={[Function]}
value=""
/>
</div>
</div>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
tooltip="Specify the region, such as for US West (Oregon) use \` us-west-2 \` as the region."
>
Default Region
</Component>
<Select
allowCustomValue={false}
autoFocus={false}
backspaceRemovesValue={true}
className="width-30"
components={
Object {
"Group": [Function],
"IndicatorsContainer": [Function],
"MenuList": [Function],
"Option": [Function],
"SingleValue": [Function],
}
}
defaultValue="us-east-2"
isClearable={false}
isDisabled={false}
isLoading={false}
isMulti={false}
isSearchable={true}
maxMenuHeight={300}
onChange={[Function]}
openMenuOnFocus={false}
options={Array []}
tabSelectsValue={true}
/>
</div>
</div>
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<Component
className="width-14"
tooltip="Namespaces of Custom Metrics."
>
Custom Metrics
</Component>
<Input
className="width-30"
onChange={[Function]}
placeholder="Namespace1,Namespace2"
value=""
/>
</div>
</div>
</div>
</Fragment>
`;

View File

@@ -0,0 +1,396 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QueryEditor should render component 1`] = `
Array [
<div
className="gf-form-inline"
>
<label
className="gf-form-label width-8 query-keyword"
>
Region
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
us-east-1
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label width-8 query-keyword"
>
Namespace
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
ec2
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label width-8 query-keyword"
>
Metric Name
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
CPUUtilization
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label width-8 query-keyword"
>
Stats
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
Average
</a>
</div>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
<i
className="fa fa-plus"
/>
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<label
className="gf-form-label width-8 query-keyword"
>
Dimensions
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
somekey
</a>
</div>
<label
className="gf-form-label query-segment-operator"
>
=
</label>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
somevalue
</a>
</div>
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
<i
className="fa fa-plus"
/>
</a>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<label
className="gf-form-label width-8 query-keyword"
>
Id
<div
className="gf-form-help-icon gf-form-help-icon--right-normal"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<i
className="fa fa-info-circle"
/>
</div>
</label>
<div
style={
Object {
"flexGrow": 1,
}
}
>
<input
className="gf-form-input gf-form-input width-8"
onBlur={[Function]}
onChange={[Function]}
value=""
/>
</div>
</div>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form-label width-8 query-keyword"
>
Expression
<div
className="gf-form-help-icon gf-form-help-icon--right-normal"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<i
className="fa fa-info-circle"
/>
</div>
</label>
<div
style={
Object {
"flexGrow": 1,
}
}
>
<input
className="gf-form-input gf-form-input"
onBlur={[MockFunction]}
onChange={[Function]}
value=""
/>
</div>
</div>
</div>,
<div
className="gf-form-inline"
>
<div
className="gf-form"
>
<label
className="gf-form-label width-8 query-keyword"
>
Min Period
<div
className="gf-form-help-icon gf-form-help-icon--right-normal"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<i
className="fa fa-info-circle"
/>
</div>
</label>
<div
style={
Object {
"flexGrow": 1,
}
}
>
<input
className="gf-form-input gf-form-input width-8"
onBlur={[MockFunction]}
onChange={[Function]}
placeholder="auto"
value=""
/>
</div>
</div>
<div
className="gf-form"
>
<label
className="gf-form-label width-8 query-keyword"
>
Alias
<div
className="gf-form-help-icon gf-form-help-icon--right-normal"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<i
className="fa fa-info-circle"
/>
</div>
</label>
<div
style={
Object {
"flexGrow": 1,
}
}
>
<input
className="gf-form-input gf-form-input width-16"
onChange={[Function]}
type="text"
value=""
/>
</div>
<div
className="gf-form-switch-container-react"
>
<label
className="gf-form gf-form-switch-container "
htmlFor="1"
>
<div
className="gf-form-label query-keyword pointer"
>
HighRes
</div>
<div
className="gf-form-switch "
>
<input
checked={false}
id="1"
onChange={[Function]}
type="checkbox"
/>
<span
className="gf-form-switch__slider"
/>
</div>
</label>
</div>
<div
className="gf-form-switch-container-react"
>
<label
className="gf-form gf-form-switch-container "
htmlFor="2"
>
<div
className="gf-form-label query-keyword pointer"
>
Match Exact
<div
className="gf-form-help-icon gf-form-help-icon--right-normal"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<i
className="fa fa-info-circle"
/>
</div>
</div>
<div
className="gf-form-switch "
>
<input
checked={true}
id="2"
onChange={[Function]}
type="checkbox"
/>
<span
className="gf-form-switch__slider"
/>
</div>
</label>
</div>
<label
className="gf-form-label"
>
<a
onClick={[Function]}
>
<i
className="fa fa-caret-right"
/>
Show
Query Preview
</a>
</label>
</div>
<div
className="gf-form gf-form--grow"
>
<div
className="gf-form-label gf-form-label--grow"
/>
</div>
</div>,
]
`;

View File

@@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Stats should render component 1`] = `
Array [
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
Average
</a>
</div>,
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
Minimum
</a>
</div>,
<div
className="gf-form"
onClick={[Function]}
>
<a
className="gf-form-label query-part"
>
<i
className="fa fa-plus"
/>
</a>
</div>,
]
`;

View File

@@ -0,0 +1,4 @@
export { Stats } from './Stats';
export { Dimensions } from './Dimensions';
export { QueryInlineField, QueryField } from './Forms';
export { Alias } from './Alias';