Commit a35b2ac4 by Erik Sundell Committed by GitHub

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
parent 29687903
...@@ -54,16 +54,20 @@ func (e *CloudWatchExecutor) executeAnnotationQuery(ctx context.Context, queryCo ...@@ -54,16 +54,20 @@ func (e *CloudWatchExecutor) executeAnnotationQuery(ctx context.Context, queryCo
alarmNames = filterAlarms(resp, namespace, metricName, dimensions, statistics, period) alarmNames = filterAlarms(resp, namespace, metricName, dimensions, statistics, period)
} else { } else {
if region == "" || namespace == "" || metricName == "" || len(statistics) == 0 { if region == "" || namespace == "" || metricName == "" || len(statistics) == 0 {
return result, nil return result, errors.New("Invalid annotations query")
} }
var qd []*cloudwatch.Dimension var qd []*cloudwatch.Dimension
for k, v := range dimensions { for k, v := range dimensions {
if vv, ok := v.(string); ok { if vv, ok := v.([]interface{}); ok {
qd = append(qd, &cloudwatch.Dimension{ for _, vvv := range vv {
Name: aws.String(k), if vvvv, ok := vvv.(string); ok {
Value: aws.String(vv), qd = append(qd, &cloudwatch.Dimension{
}) Name: aws.String(k),
Value: aws.String(vvvv),
})
}
}
} }
} }
for _, s := range statistics { for _, s := range statistics {
......
import { react2AngularDirective } from 'app/core/utils/react2angular'; import { react2AngularDirective } from 'app/core/utils/react2angular';
import { QueryEditor as StackdriverQueryEditor } from 'app/plugins/datasource/stackdriver/components/QueryEditor'; 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 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 PageHeader from './components/PageHeader/PageHeader';
import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
import { TagFilter } from './components/TagFilter/TagFilter'; import { TagFilter } from './components/TagFilter/TagFilter';
...@@ -93,6 +94,11 @@ export function registerAngularDirectives() { ...@@ -93,6 +94,11 @@ export function registerAngularDirectives() {
['datasource', { watchDepth: 'reference' }], ['datasource', { watchDepth: 'reference' }],
['templateSrv', { watchDepth: 'reference' }], ['templateSrv', { watchDepth: 'reference' }],
]); ]);
react2AngularDirective('cloudwatchAnnotationQueryEditor', CloudWatchAnnotationQueryEditor, [
'query',
'onChange',
['datasource', { watchDepth: 'reference' }],
]);
react2AngularDirective('secretFormField', SecretFormField, [ react2AngularDirective('secretFormField', SecretFormField, [
'value', 'value',
'isConfigured', 'isConfigured',
......
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);
}
}
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>
</>
);
}
import React, { PureComponent, ChangeEvent } from 'react'; import React, { PureComponent, ChangeEvent } from 'react';
import { SelectableValue, ExploreQueryFieldProps } from '@grafana/data'; import { ExploreQueryFieldProps } from '@grafana/data';
import { Input, Segment, SegmentAsync, ValidationEvents, EventsWithValidation, Switch } from '@grafana/ui'; import { Input, ValidationEvents, EventsWithValidation, Switch } from '@grafana/ui';
import { CloudWatchQuery } from '../types'; import { CloudWatchQuery } from '../types';
import CloudWatchDatasource from '../datasource'; import CloudWatchDatasource from '../datasource';
import { SelectableStrings } from '../types'; import { QueryField, Alias, QueryFieldsEditor } from './';
import { Stats, Dimensions, QueryInlineField, QueryField, Alias } from './';
export type Props = ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery>; export type Props = ExploreQueryFieldProps<CloudWatchDatasource, CloudWatchQuery>;
interface State { interface State {
regions: SelectableStrings;
namespaces: SelectableStrings;
metricNames: SelectableStrings;
variableOptionGroup: SelectableValue<string>;
showMeta: boolean; showMeta: boolean;
} }
...@@ -26,7 +21,7 @@ const idValidationEvents: ValidationEvents = { ...@@ -26,7 +21,7 @@ const idValidationEvents: ValidationEvents = {
}; };
export class QueryEditor extends PureComponent<Props, State> { 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) { static getDerivedStateFromProps(props: Props, state: State) {
const { query } = props; const { query } = props;
...@@ -70,128 +65,32 @@ export class QueryEditor extends PureComponent<Props, State> { ...@@ -70,128 +65,32 @@ export class QueryEditor extends PureComponent<Props, State> {
return 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) { onChange(query: CloudWatchQuery) {
const { onChange, onRunQuery } = this.props; const { onChange, onRunQuery } = this.props;
onChange(query); onChange(query);
onRunQuery(); 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() { render() {
const { query, datasource, onChange, onRunQuery, data } = this.props; const { data, query, onRunQuery } = this.props;
const { regions, namespaces, variableOptionGroup: variableOptionGroup, showMeta } = this.state; const { showMeta } = this.state;
const metaDataExist = data && Object.values(data).length && data.state === 'Done'; const metaDataExist = data && Object.values(data).length && data.state === 'Done';
return ( return (
<> <>
<QueryInlineField label="Region"> <QueryFieldsEditor {...this.props}></QueryFieldsEditor>
<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>
</>
)}
{query.statistics.length <= 1 && ( {query.statistics.length <= 1 && (
<div className="gf-form-inline"> <div className="gf-form-inline">
<div className="gf-form"> <div className="gf-form">
<QueryField <QueryField
className="query-keyword"
label="Id" label="Id"
tooltip="Id can include numbers, letters, and underscore, and must start with a lowercase letter." tooltip="Id can include numbers, letters, and underscore, and must start with a lowercase letter."
> >
<Input <Input
className="gf-form-input width-8" className="gf-form-input width-8"
onBlur={onRunQuery} 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} validationEvents={idValidationEvents}
value={query.id || ''} value={query.id || ''}
/> />
...@@ -208,7 +107,7 @@ export class QueryEditor extends PureComponent<Props, State> { ...@@ -208,7 +107,7 @@ export class QueryEditor extends PureComponent<Props, State> {
onBlur={onRunQuery} onBlur={onRunQuery}
value={query.expression || ''} value={query.expression || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange={(event: ChangeEvent<HTMLInputElement>) =>
onChange({ ...query, expression: event.target.value }) this.onChange({ ...query, expression: event.target.value })
} }
/> />
</QueryField> </QueryField>
...@@ -217,19 +116,20 @@ export class QueryEditor extends PureComponent<Props, State> { ...@@ -217,19 +116,20 @@ export class QueryEditor extends PureComponent<Props, State> {
)} )}
<div className="gf-form-inline"> <div className="gf-form-inline">
<div className="gf-form"> <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 <Input
className="gf-form-input width-8" className="gf-form-input width-8"
value={query.period || ''} value={query.period || ''}
placeholder="auto" placeholder="auto"
onBlur={onRunQuery} onBlur={onRunQuery}
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange({ ...query, period: event.target.value })} onChange={(event: ChangeEvent<HTMLInputElement>) =>
this.onChange({ ...query, period: event.target.value })
}
/> />
</QueryField> </QueryField>
</div> </div>
<div className="gf-form"> <div className="gf-form">
<QueryField <QueryField
className="query-keyword"
label="Alias" label="Alias"
tooltip="Alias replacement variables: {{metric}}, {{stat}}, {{namespace}}, {{region}}, {{period}}, {{label}}, {{YOUR_DIMENSION_NAME}}" tooltip="Alias replacement variables: {{metric}}, {{stat}}, {{namespace}}, {{region}}, {{period}}, {{label}}, {{YOUR_DIMENSION_NAME}}"
> >
...@@ -247,7 +147,6 @@ export class QueryEditor extends PureComponent<Props, State> { ...@@ -247,7 +147,6 @@ export class QueryEditor extends PureComponent<Props, State> {
onClick={() => onClick={() =>
metaDataExist && metaDataExist &&
this.setState({ this.setState({
...this.state,
showMeta: !showMeta, showMeta: !showMeta,
}) })
} }
......
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>
</>
)}
</>
);
}
...@@ -2,3 +2,4 @@ export { Stats } from './Stats'; ...@@ -2,3 +2,4 @@ export { Stats } from './Stats';
export { Dimensions } from './Dimensions'; export { Dimensions } from './Dimensions';
export { QueryInlineField, QueryField } from './Forms'; export { QueryInlineField, QueryField } from './Forms';
export { Alias } from './Alias'; export { Alias } from './Alias';
export { QueryFieldsEditor } from './QueryFieldsEditor';
...@@ -3,12 +3,9 @@ import { DataSourcePlugin } from '@grafana/data'; ...@@ -3,12 +3,9 @@ import { DataSourcePlugin } from '@grafana/data';
import { ConfigEditor } from './components/ConfigEditor'; import { ConfigEditor } from './components/ConfigEditor';
import { QueryEditor } from './components/QueryEditor'; import { QueryEditor } from './components/QueryEditor';
import CloudWatchDatasource from './datasource'; import CloudWatchDatasource from './datasource';
import { CloudWatchAnnotationsQueryCtrl } from './annotations_query_ctrl';
import { CloudWatchJsonData, CloudWatchQuery } from './types'; import { CloudWatchJsonData, CloudWatchQuery } from './types';
class CloudWatchAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
}
export const plugin = new DataSourcePlugin<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>( export const plugin = new DataSourcePlugin<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>(
CloudWatchDatasource CloudWatchDatasource
) )
......
<cloudwatch-query-parameter target="ctrl.annotation" datasource="ctrl.datasource"></cloudwatch-query-parameter> <cloudwatch-annotation-query-editor
datasource="ctrl.datasource"
<div class="editor-row" style="padding: 2rem 0"> on-change="ctrl.onChange"
<div class="section"> query="ctrl.annotation"
<h5>Prefix matching</h5> ></cloudwatch-annotation-query-editor>
<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>
...@@ -13,6 +13,12 @@ export interface CloudWatchQuery extends DataQuery { ...@@ -13,6 +13,12 @@ export interface CloudWatchQuery extends DataQuery {
matchExact: boolean; matchExact: boolean;
} }
export interface AnnotationQuery extends CloudWatchQuery {
prefixMatching: boolean;
actionPrefix: string;
alarmNamePrefix: string;
}
export type SelectableStrings = Array<SelectableValue<string>>; export type SelectableStrings = Array<SelectableValue<string>>;
export interface CloudWatchJsonData extends DataSourceJsonData { export interface CloudWatchJsonData extends DataSourceJsonData {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment