CloudWatch: Annotation Editor rewrite (#20765)

* wip: react rewrite

* Cleanup

* Break out non annontations specific fields

* Cleanup. Make annontations editor a functional component

* Remove redundant classnames

* Add paneldata to props

* Cleanup

* Fix rebase merge problem

* Updates after pr feedback

* Fix conflict with master
This commit is contained in:
Erik Sundell 2020-01-15 16:38:15 +01:00 committed by GitHub
parent 29687903f8
commit a35b2ac463
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 279 additions and 144 deletions

View File

@ -54,16 +54,20 @@ func (e *CloudWatchExecutor) executeAnnotationQuery(ctx context.Context, queryCo
alarmNames = filterAlarms(resp, namespace, metricName, dimensions, statistics, period)
} else {
if region == "" || namespace == "" || metricName == "" || len(statistics) == 0 {
return result, nil
return result, errors.New("Invalid annotations query")
}
var qd []*cloudwatch.Dimension
for k, v := range dimensions {
if vv, ok := v.(string); ok {
qd = append(qd, &cloudwatch.Dimension{
Name: aws.String(k),
Value: aws.String(vv),
})
if vv, ok := v.([]interface{}); ok {
for _, vvv := range vv {
if vvvv, ok := vvv.(string); ok {
qd = append(qd, &cloudwatch.Dimension{
Name: aws.String(k),
Value: aws.String(vvvv),
})
}
}
}
}
for _, s := range statistics {

View File

@ -1,6 +1,7 @@
import { react2AngularDirective } from 'app/core/utils/react2angular';
import { QueryEditor as StackdriverQueryEditor } from 'app/plugins/datasource/stackdriver/components/QueryEditor';
import { AnnotationQueryEditor as StackdriverAnnotationQueryEditor } from 'app/plugins/datasource/stackdriver/components/AnnotationQueryEditor';
import { AnnotationQueryEditor as CloudWatchAnnotationQueryEditor } from 'app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor';
import PageHeader from './components/PageHeader/PageHeader';
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
import { TagFilter } from './components/TagFilter/TagFilter';
@ -93,6 +94,11 @@ export function registerAngularDirectives() {
['datasource', { watchDepth: 'reference' }],
['templateSrv', { watchDepth: 'reference' }],
]);
react2AngularDirective('cloudwatchAnnotationQueryEditor', CloudWatchAnnotationQueryEditor, [
'query',
'onChange',
['datasource', { watchDepth: 'reference' }],
]);
react2AngularDirective('secretFormField', SecretFormField, [
'value',
'isConfigured',

View File

@ -0,0 +1,31 @@
import _ from 'lodash';
import { AnnotationQuery } from './types';
export class CloudWatchAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
annotation: any;
/** @ngInject */
constructor() {
_.defaultsDeep(this.annotation, {
namespace: '',
metricName: '',
expression: '',
dimensions: {},
region: 'default',
id: '',
alias: '',
statistics: ['Average'],
matchExact: true,
prefixMatching: false,
actionPrefix: '',
alarmNamePrefix: '',
});
this.onChange = this.onChange.bind(this);
}
onChange(query: AnnotationQuery) {
Object.assign(this.annotation, query);
}
}

View File

@ -0,0 +1,60 @@
import React, { ChangeEvent } from 'react';
import { Switch } from '@grafana/ui';
import { PanelData } from '@grafana/data';
import { CloudWatchQuery, AnnotationQuery } from '../types';
import CloudWatchDatasource from '../datasource';
import { QueryField, QueryFieldsEditor } from './';
export type Props = {
query: AnnotationQuery;
datasource: CloudWatchDatasource;
onChange: (value: AnnotationQuery) => void;
data?: PanelData;
};
export function AnnotationQueryEditor(props: React.PropsWithChildren<Props>) {
const { query, onChange } = props;
return (
<>
<QueryFieldsEditor
{...props}
onChange={(editorQuery: CloudWatchQuery) => onChange({ ...query, ...editorQuery })}
hideWilcard
></QueryFieldsEditor>
<div className="gf-form-inline">
<Switch
label="Enable Prefix Matching"
labelClass="query-keyword"
checked={query.prefixMatching}
onChange={() => onChange({ ...query, prefixMatching: !query.prefixMatching })}
/>
<div className="gf-form gf-form--grow">
<QueryField label="Action">
<input
disabled={!query.prefixMatching}
className="gf-form-input width-12"
value={query.actionPrefix || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
onChange({ ...query, actionPrefix: event.target.value })
}
/>
</QueryField>
<QueryField label="Alarm Name">
<input
disabled={!query.prefixMatching}
className="gf-form-input width-12"
value={query.alarmNamePrefix || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
onChange({ ...query, alarmNamePrefix: event.target.value })
}
/>
</QueryField>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
</div>
</div>
</>
);
}

View File

@ -1,18 +1,13 @@
import React, { PureComponent, ChangeEvent } from 'react';
import { SelectableValue, ExploreQueryFieldProps } from '@grafana/data';
import { Input, Segment, SegmentAsync, ValidationEvents, EventsWithValidation, Switch } from '@grafana/ui';
import { ExploreQueryFieldProps } from '@grafana/data';
import { Input, 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 './';
import { QueryField, Alias, QueryFieldsEditor } from './';
export type Props = ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery>;
interface State {
regions: SelectableStrings;
namespaces: SelectableStrings;
metricNames: SelectableStrings;
variableOptionGroup: SelectableValue<string>;
showMeta: boolean;
}
@ -26,7 +21,7 @@ const idValidationEvents: ValidationEvents = {
};
export class QueryEditor extends PureComponent<Props, State> {
state: State = { regions: [], namespaces: [], metricNames: [], variableOptionGroup: {}, showMeta: false };
state: State = { showMeta: false };
static getDerivedStateFromProps(props: Props, state: State) {
const { query } = props;
@ -70,128 +65,32 @@ export class QueryEditor extends PureComponent<Props, State> {
return state;
}
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();
}
// Load dimension values based on current selected dimensions.
// Remove the new dimension key and all dimensions that has a wildcard as selected value
loadDimensionValues = (newKey: string) => {
const { datasource, query } = this.props;
const { [newKey]: value, ...dim } = query.dimensions;
const newDimensions = Object.entries(dim).reduce(
(result, [key, value]) => (value === '*' ? result : { ...result, [key]: value }),
{}
);
return datasource
.getDimensionValues(query.region, query.namespace, query.metricName, newKey, newDimensions)
.then(values => (values.length ? [{ value: '*', text: '*', label: '*' }, ...values] : values))
.then(this.appendTemplateVariables);
};
render() {
const { query, datasource, onChange, onRunQuery, data } = this.props;
const { regions, namespaces, variableOptionGroup: variableOptionGroup, showMeta } = this.state;
const { data, query, onRunQuery } = this.props;
const { showMeta } = this.state;
const metaDataExist = data && Object.values(data).length && data.state === 'Done';
return (
<>
<QueryInlineField label="Region">
<Segment
value={query.region}
placeholder="Select region"
options={regions}
allowCustomValue
onChange={({ value: region }) => this.onChange({ ...query, region })}
/>
</QueryInlineField>
{query.expression.length === 0 && (
<>
<QueryInlineField label="Namespace">
<Segment
value={query.namespace}
placeholder="Select namespace"
allowCustomValue
options={namespaces}
onChange={({ value: namespace }) => this.onChange({ ...query, namespace })}
/>
</QueryInlineField>
<QueryInlineField label="Metric Name">
<SegmentAsync
value={query.metricName}
placeholder="Select metric name"
allowCustomValue
loadOptions={this.loadMetricNames}
onChange={({ value: 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={this.loadDimensionValues}
/>
</QueryInlineField>
</>
)}
<QueryFieldsEditor {...this.props}></QueryFieldsEditor>
{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 })}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
this.onChange({ ...query, id: event.target.value })
}
validationEvents={idValidationEvents}
value={query.id || ''}
/>
@ -208,7 +107,7 @@ export class QueryEditor extends PureComponent<Props, State> {
onBlur={onRunQuery}
value={query.expression || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
onChange({ ...query, expression: event.target.value })
this.onChange({ ...query, expression: event.target.value })
}
/>
</QueryField>
@ -217,19 +116,20 @@ export class QueryEditor extends PureComponent<Props, State> {
)}
<div className="gf-form-inline">
<div className="gf-form">
<QueryField className="query-keyword" label="Period" tooltip="Minimum interval between points in seconds">
<QueryField label="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 })}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
this.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}}"
>
@ -247,7 +147,6 @@ export class QueryEditor extends PureComponent<Props, State> {
onClick={() =>
metaDataExist &&
this.setState({
...this.state,
showMeta: !showMeta,
})
}

View File

@ -0,0 +1,144 @@
import React, { useState, useEffect } from 'react';
import { SelectableValue } from '@grafana/data';
import { Segment, SegmentAsync } from '@grafana/ui';
import { CloudWatchQuery, SelectableStrings } from '../types';
import CloudWatchDatasource from '../datasource';
import { Stats, Dimensions, QueryInlineField } from './';
export type Props = {
query: CloudWatchQuery;
datasource: CloudWatchDatasource;
onRunQuery?: () => void;
onChange: (value: CloudWatchQuery) => void;
hideWilcard?: boolean;
};
interface State {
regions: SelectableStrings;
namespaces: SelectableStrings;
metricNames: SelectableStrings;
variableOptionGroup: SelectableValue<string>;
showMeta: boolean;
}
export function QueryFieldsEditor({
query,
datasource,
onChange,
onRunQuery = () => {},
hideWilcard = false,
}: React.PropsWithChildren<Props>) {
const [state, setState] = useState<State>({
regions: [],
namespaces: [],
metricNames: [],
variableOptionGroup: {},
showMeta: false,
});
useEffect(() => {
const variableOptionGroup = {
label: 'Template Variables',
options: datasource.variables.map(toOption),
};
Promise.all([datasource.metricFindQuery('regions()'), datasource.metricFindQuery('namespaces()')]).then(
([regions, namespaces]) => {
setState({
...state,
regions: [...regions, variableOptionGroup],
namespaces: [...namespaces, variableOptionGroup],
variableOptionGroup,
});
}
);
}, []);
const loadMetricNames = async () => {
const { namespace, region } = query;
return datasource.metricFindQuery(`metrics(${namespace},${region})`).then(appendTemplateVariables);
};
const appendTemplateVariables = (values: SelectableValue[]) => [
...values,
{ label: 'Template Variables', options: datasource.variables.map(toOption) },
];
const toOption = (value: any) => ({ label: value, value });
const onQueryChange = (query: CloudWatchQuery) => {
onChange(query);
onRunQuery();
};
// Load dimension values based on current selected dimensions.
// Remove the new dimension key and all dimensions that has a wildcard as selected value
const loadDimensionValues = (newKey: string) => {
const { [newKey]: value, ...dim } = query.dimensions;
const newDimensions = Object.entries(dim).reduce(
(result, [key, value]) => (value === '*' ? result : { ...result, [key]: value }),
{}
);
return datasource
.getDimensionValues(query.region, query.namespace, query.metricName, newKey, newDimensions)
.then(values => (values.length ? [{ value: '*', text: '*', label: '*' }, ...values] : values))
.then(appendTemplateVariables);
};
const { regions, namespaces, variableOptionGroup } = state;
return (
<>
<QueryInlineField label="Region">
<Segment
value={query.region}
placeholder="Select region"
options={regions}
allowCustomValue
onChange={({ value: region }) => onChange({ ...query, region })}
/>
</QueryInlineField>
{query.expression.length === 0 && (
<>
<QueryInlineField label="Namespace">
<Segment
value={query.namespace}
placeholder="Select namespace"
allowCustomValue
options={namespaces}
onChange={({ value: namespace }) => onChange({ ...query, namespace })}
/>
</QueryInlineField>
<QueryInlineField label="Metric Name">
<SegmentAsync
value={query.metricName}
placeholder="Select metric name"
allowCustomValue
loadOptions={loadMetricNames}
onChange={({ value: metricName }) => onChange({ ...query, metricName })}
/>
</QueryInlineField>
<QueryInlineField label="Stats">
<Stats
stats={datasource.standardStatistics.map(toOption)}
values={query.statistics}
onChange={statistics => onQueryChange({ ...query, statistics })}
variableOptionGroup={variableOptionGroup}
/>
</QueryInlineField>
<QueryInlineField label="Dimensions">
<Dimensions
dimensions={query.dimensions}
onChange={dimensions => onQueryChange({ ...query, dimensions })}
loadKeys={() => datasource.getDimensionKeys(query.namespace, query.region).then(appendTemplateVariables)}
loadValues={loadDimensionValues}
/>
</QueryInlineField>
</>
)}
</>
);
}

View File

@ -2,3 +2,4 @@ export { Stats } from './Stats';
export { Dimensions } from './Dimensions';
export { QueryInlineField, QueryField } from './Forms';
export { Alias } from './Alias';
export { QueryFieldsEditor } from './QueryFieldsEditor';

View File

@ -3,12 +3,9 @@ import { DataSourcePlugin } from '@grafana/data';
import { ConfigEditor } from './components/ConfigEditor';
import { QueryEditor } from './components/QueryEditor';
import CloudWatchDatasource from './datasource';
import { CloudWatchAnnotationsQueryCtrl } from './annotations_query_ctrl';
import { CloudWatchJsonData, CloudWatchQuery } from './types';
class CloudWatchAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
}
export const plugin = new DataSourcePlugin<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>(
CloudWatchDatasource
)

View File

@ -1,18 +1,5 @@
<cloudwatch-query-parameter target="ctrl.annotation" datasource="ctrl.datasource"></cloudwatch-query-parameter>
<div class="editor-row" style="padding: 2rem 0">
<div class="section">
<h5>Prefix matching</h5>
<div class="gf-form-inline">
<gf-form-switch class="gf-form" label="Enable" checked="ctrl.annotation.prefixMatching" switch-class="max-width-6"></gf-form-switch>
<div class="gf-form" ng-if="ctrl.annotation.prefixMatching">
<span class="gf-form-label">Action</span>
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.actionPrefix'></input>
</div>
<div class="gf-form" ng-if="ctrl.annotation.prefixMatching">
<span class="gf-form-label">Alarm Name</span>
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.alarmNamePrefix'></input>
</div>
</div>
</div>
</div>
<cloudwatch-annotation-query-editor
datasource="ctrl.datasource"
on-change="ctrl.onChange"
query="ctrl.annotation"
></cloudwatch-annotation-query-editor>

View File

@ -13,6 +13,12 @@ export interface CloudWatchQuery extends DataQuery {
matchExact: boolean;
}
export interface AnnotationQuery extends CloudWatchQuery {
prefixMatching: boolean;
actionPrefix: string;
alarmNamePrefix: string;
}
export type SelectableStrings = Array<SelectableValue<string>>;
export interface CloudWatchJsonData extends DataSourceJsonData {