From a35b2ac46347507b0e438f8603798de2dad658cb Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 15 Jan 2020 16:38:15 +0100 Subject: [PATCH] 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 --- pkg/tsdb/cloudwatch/annotation_query.go | 16 +- public/app/core/angular_wrappers.ts | 6 + .../cloudwatch/annotations_query_ctrl.ts | 31 ++++ .../components/AnnotationQueryEditor.tsx | 60 ++++++++ .../cloudwatch/components/QueryEditor.tsx | 131 ++-------------- .../components/QueryFieldsEditor.tsx | 144 ++++++++++++++++++ .../datasource/cloudwatch/components/index.ts | 1 + .../plugins/datasource/cloudwatch/module.tsx | 5 +- .../partials/annotations.editor.html | 23 +-- .../plugins/datasource/cloudwatch/types.ts | 6 + 10 files changed, 279 insertions(+), 144 deletions(-) create mode 100644 public/app/plugins/datasource/cloudwatch/annotations_query_ctrl.ts create mode 100644 public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.tsx create mode 100644 public/app/plugins/datasource/cloudwatch/components/QueryFieldsEditor.tsx diff --git a/pkg/tsdb/cloudwatch/annotation_query.go b/pkg/tsdb/cloudwatch/annotation_query.go index 7c6465f278c..bd99f6377f1 100644 --- a/pkg/tsdb/cloudwatch/annotation_query.go +++ b/pkg/tsdb/cloudwatch/annotation_query.go @@ -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 { diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 8cf3ef5b48b..92657fd80e7 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -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', diff --git a/public/app/plugins/datasource/cloudwatch/annotations_query_ctrl.ts b/public/app/plugins/datasource/cloudwatch/annotations_query_ctrl.ts new file mode 100644 index 00000000000..34fc5cfbfa6 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/annotations_query_ctrl.ts @@ -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); + } +} diff --git a/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.tsx new file mode 100644 index 00000000000..a5fdc14c967 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.tsx @@ -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) { + const { query, onChange } = props; + return ( + <> + onChange({ ...query, ...editorQuery })} + hideWilcard + > +
+ onChange({ ...query, prefixMatching: !query.prefixMatching })} + /> + +
+ + ) => + onChange({ ...query, actionPrefix: event.target.value }) + } + /> + + + ) => + onChange({ ...query, alarmNamePrefix: event.target.value }) + } + /> + +
+
+
+
+
+ + ); +} diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.tsx index 93acea348b1..e017f6b0387 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor.tsx @@ -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; interface State { - regions: SelectableStrings; - namespaces: SelectableStrings; - metricNames: SelectableStrings; - variableOptionGroup: SelectableValue; showMeta: boolean; } @@ -26,7 +21,7 @@ const idValidationEvents: ValidationEvents = { }; export class QueryEditor extends PureComponent { - 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 { 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 ( <> - - this.onChange({ ...query, region })} - /> - - - {query.expression.length === 0 && ( - <> - - this.onChange({ ...query, namespace })} - /> - - - - this.onChange({ ...query, metricName })} - /> - - - - this.onChange({ ...query, statistics })} - variableOptionGroup={variableOptionGroup} - /> - - - - this.onChange({ ...query, dimensions })} - loadKeys={() => - datasource.getDimensionKeys(query.namespace, query.region).then(this.appendTemplateVariables) - } - loadValues={this.loadDimensionValues} - /> - - - )} + {query.statistics.length <= 1 && (
) => onChange({ ...query, id: event.target.value })} + onChange={(event: ChangeEvent) => + this.onChange({ ...query, id: event.target.value }) + } validationEvents={idValidationEvents} value={query.id || ''} /> @@ -208,7 +107,7 @@ export class QueryEditor extends PureComponent { onBlur={onRunQuery} value={query.expression || ''} onChange={(event: ChangeEvent) => - onChange({ ...query, expression: event.target.value }) + this.onChange({ ...query, expression: event.target.value }) } /> @@ -217,19 +116,20 @@ export class QueryEditor extends PureComponent { )}
- + ) => onChange({ ...query, period: event.target.value })} + onChange={(event: ChangeEvent) => + this.onChange({ ...query, period: event.target.value }) + } />
@@ -247,7 +147,6 @@ export class QueryEditor extends PureComponent { onClick={() => metaDataExist && this.setState({ - ...this.state, showMeta: !showMeta, }) } diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryFieldsEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryFieldsEditor.tsx new file mode 100644 index 00000000000..5aa9cef3bb0 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/components/QueryFieldsEditor.tsx @@ -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; + showMeta: boolean; +} + +export function QueryFieldsEditor({ + query, + datasource, + onChange, + onRunQuery = () => {}, + hideWilcard = false, +}: React.PropsWithChildren) { + const [state, setState] = useState({ + 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 ( + <> + + onChange({ ...query, region })} + /> + + + {query.expression.length === 0 && ( + <> + + onChange({ ...query, namespace })} + /> + + + + onChange({ ...query, metricName })} + /> + + + + onQueryChange({ ...query, statistics })} + variableOptionGroup={variableOptionGroup} + /> + + + + onQueryChange({ ...query, dimensions })} + loadKeys={() => datasource.getDimensionKeys(query.namespace, query.region).then(appendTemplateVariables)} + loadValues={loadDimensionValues} + /> + + + )} + + ); +} diff --git a/public/app/plugins/datasource/cloudwatch/components/index.ts b/public/app/plugins/datasource/cloudwatch/components/index.ts index 8bb770ce20d..e9c1bfed2d0 100644 --- a/public/app/plugins/datasource/cloudwatch/components/index.ts +++ b/public/app/plugins/datasource/cloudwatch/components/index.ts @@ -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'; diff --git a/public/app/plugins/datasource/cloudwatch/module.tsx b/public/app/plugins/datasource/cloudwatch/module.tsx index f341fea986d..c818a78bf96 100644 --- a/public/app/plugins/datasource/cloudwatch/module.tsx +++ b/public/app/plugins/datasource/cloudwatch/module.tsx @@ -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 ) diff --git a/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html b/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html index 189aa9226fa..926d939bd70 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html +++ b/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html @@ -1,18 +1,5 @@ - - -
-
-
Prefix matching
-
- -
- Action - -
-
- Alarm Name - -
-
-
-
+ diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts index 84d034c14fe..a070589645d 100644 --- a/public/app/plugins/datasource/cloudwatch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/types.ts @@ -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>; export interface CloudWatchJsonData extends DataSourceJsonData {