From f58a2d879ec8b6c1c959f99eea5ec929de5f8502 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Wed, 27 Apr 2022 12:41:48 +0200 Subject: [PATCH] Cloudwatch: Use new annotation API (#48102) * use new annotation api * fix bad merge --- pkg/tsdb/cloudwatch/annotation_query.go | 27 +-- public/app/angular/angular_wrappers.ts | 1 - .../dashboard/state/DashboardMigrator.ts | 6 +- .../cloudwatch/annotationSupport.test.ts | 167 ++++++++++++++++++ .../cloudwatch/annotationSupport.ts | 60 +++++++ .../cloudwatch/annotations_query_ctrl.ts | 34 ---- .../components/AnnotationQueryEditor.test.tsx | 28 +-- .../components/AnnotationQueryEditor.tsx | 33 ++-- .../components/Dimensions/Dimensions.test.tsx | 8 +- .../components/Dimensions/Dimensions.tsx | 26 +-- .../components/Dimensions/FilterItem.tsx | 8 +- .../cloudwatch/components/LogsQueryField.tsx | 6 +- .../MetricStatEditor.test.tsx | 50 +++--- .../MetricStatEditor/MetricStatEditor.tsx | 58 +++--- .../components/MetricsQueryEditor.tsx | 12 +- .../components/QueryHeader.test.tsx | 4 +- .../cloudwatch/components/QueryHeader.tsx | 4 +- .../VariableQueryEditor.tsx | 2 +- .../datasource/cloudwatch/datasource.ts | 109 ++++++------ .../plugins/datasource/cloudwatch/guards.ts | 19 +- .../datasource/cloudwatch/migration.test.ts | 24 +-- .../datasource/cloudwatch/migrations.ts | 10 +- .../plugins/datasource/cloudwatch/module.tsx | 4 +- .../partials/annotations.editor.html | 5 - .../cloudwatch/partials/query.parameter.html | 94 ---------- .../cloudwatch/specs/datasource.test.ts | 8 +- .../plugins/datasource/cloudwatch/types.ts | 101 ++++++----- 27 files changed, 530 insertions(+), 378 deletions(-) create mode 100644 public/app/plugins/datasource/cloudwatch/annotationSupport.test.ts create mode 100644 public/app/plugins/datasource/cloudwatch/annotationSupport.ts delete mode 100644 public/app/plugins/datasource/cloudwatch/annotations_query_ctrl.ts delete mode 100644 public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html delete mode 100644 public/app/plugins/datasource/cloudwatch/partials/query.parameter.html diff --git a/pkg/tsdb/cloudwatch/annotation_query.go b/pkg/tsdb/cloudwatch/annotation_query.go index 0a62d691157..e03cde1bdd2 100644 --- a/pkg/tsdb/cloudwatch/annotation_query.go +++ b/pkg/tsdb/cloudwatch/annotation_query.go @@ -12,6 +12,13 @@ import ( "github.com/grafana/grafana/pkg/util/errutil" ) +type annotationEvent struct { + Title string + Time time.Time + Tags string + Text string +} + func (e *cloudWatchExecutor) executeAnnotationQuery(pluginCtx backend.PluginContext, model *simplejson.Json, query backend.DataQuery) (*backend.QueryDataResponse, error) { result := backend.NewQueryDataResponse() @@ -79,7 +86,7 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(pluginCtx backend.PluginCont } } - annotations := make([]map[string]string, 0) + annotations := make([]*annotationEvent, 0) for _, alarmName := range alarmNames { params := &cloudwatch.DescribeAlarmHistoryInput{ AlarmName: alarmName, @@ -92,12 +99,12 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(pluginCtx backend.PluginCont return nil, errutil.Wrap("failed to call cloudwatch:DescribeAlarmHistory", err) } for _, history := range resp.AlarmHistoryItems { - annotation := make(map[string]string) - annotation["time"] = history.Timestamp.UTC().Format(time.RFC3339) - annotation["title"] = *history.AlarmName - annotation["tags"] = *history.HistoryItemType - annotation["text"] = *history.HistorySummary - annotations = append(annotations, annotation) + annotations = append(annotations, &annotationEvent{ + Time: *history.Timestamp, + Title: *history.AlarmName, + Tags: *history.HistoryItemType, + Text: *history.HistorySummary, + }) } } @@ -108,16 +115,16 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(pluginCtx backend.PluginCont return result, err } -func transformAnnotationToTable(annotations []map[string]string, query backend.DataQuery) *data.Frame { +func transformAnnotationToTable(annotations []*annotationEvent, query backend.DataQuery) *data.Frame { frame := data.NewFrame(query.RefID, - data.NewField("time", nil, []string{}), + data.NewField("time", nil, []time.Time{}), data.NewField("title", nil, []string{}), data.NewField("tags", nil, []string{}), data.NewField("text", nil, []string{}), ) for _, a := range annotations { - frame.AppendRow(a["time"], a["title"], a["tags"], a["text"]) + frame.AppendRow(a.Time, a.Title, a.Tags, a.Text) } frame.Meta = &data.FrameMeta{ diff --git a/public/app/angular/angular_wrappers.ts b/public/app/angular/angular_wrappers.ts index 11d77c90a1d..bb9f7a845dd 100644 --- a/public/app/angular/angular_wrappers.ts +++ b/public/app/angular/angular_wrappers.ts @@ -123,7 +123,6 @@ export function registerAngularDirectives() { ['datasource', { watchDepth: 'reference' }], ['templateSrv', { watchDepth: 'reference' }], ]); - react2AngularDirective('secretFormField', SecretFormField, [ 'value', 'isConfigured', diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index f9e5be516c0..20a4b7265aa 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -45,7 +45,7 @@ import { migrateMultipleStatsAnnotationQuery, migrateMultipleStatsMetricsQuery, } from 'app/plugins/datasource/cloudwatch/migrations'; -import { CloudWatchAnnotationQuery, CloudWatchMetricsQuery } from 'app/plugins/datasource/cloudwatch/types'; +import { CloudWatchMetricsQuery, LegacyAnnotationQuery } from 'app/plugins/datasource/cloudwatch/types'; import { plugin as gaugePanelPlugin } from 'app/plugins/panel/gauge/module'; import { plugin as statPanelPlugin } from 'app/plugins/panel/stat/module'; @@ -1170,7 +1170,9 @@ function isCloudWatchQuery(target: DataQuery): target is CloudWatchMetricsQuery ); } -function isLegacyCloudWatchAnnotationQuery(target: AnnotationQuery): target is CloudWatchAnnotationQuery { +function isLegacyCloudWatchAnnotationQuery( + target: AnnotationQuery +): target is AnnotationQuery { return ( target.hasOwnProperty('dimensions') && target.hasOwnProperty('namespace') && diff --git a/public/app/plugins/datasource/cloudwatch/annotationSupport.test.ts b/public/app/plugins/datasource/cloudwatch/annotationSupport.test.ts new file mode 100644 index 00000000000..6bd10edb54c --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/annotationSupport.test.ts @@ -0,0 +1,167 @@ +import { AnnotationQuery } from '@grafana/data'; + +import { CloudWatchAnnotationSupport } from './annotationSupport'; +import { CloudWatchAnnotationQuery, LegacyAnnotationQuery } from './types'; + +const metricStatAnnotationQuery: CloudWatchAnnotationQuery = { + queryMode: 'Annotations', + region: 'us-east-2', + namespace: 'AWS/EC2', + period: '300', + metricName: 'CPUUtilization', + dimensions: { InstanceId: 'i-123' }, + matchExact: true, + statistic: 'Average', + refId: 'anno', + prefixMatching: false, + actionPrefix: '', + alarmNamePrefix: '', +}; + +const prefixMatchingAnnotationQuery: CloudWatchAnnotationQuery = { + queryMode: 'Annotations', + region: 'us-east-2', + namespace: '', + period: '300', + metricName: '', + dimensions: undefined, + statistic: 'Average', + refId: 'anno', + prefixMatching: true, + actionPrefix: 'arn', + alarmNamePrefix: 'test-alarm', +}; + +const annotationQuery: AnnotationQuery = { + name: 'Anno', + enable: false, + iconColor: '', + target: metricStatAnnotationQuery!, +}; + +const legacyAnnotationQuery: LegacyAnnotationQuery = { + name: 'Anno', + enable: false, + iconColor: '', + region: '', + namespace: 'AWS/EC2', + period: '300', + metricName: 'CPUUtilization', + dimensions: { InstanceId: 'i-123' }, + matchExact: true, + statistic: '', + refId: '', + prefixMatching: false, + actionPrefix: '', + alarmNamePrefix: '', + target: { + limit: 0, + matchAny: false, + tags: [], + type: '', + }, + alias: '', + builtIn: 0, + datasource: undefined, + expression: '', + hide: false, + id: '', + type: '', + statistics: [], +}; + +describe('annotationSupport', () => { + describe('when prepareAnnotation', () => { + describe('is being called with new style annotations', () => { + it('should return the same query without changing it', () => { + const preparedAnnotation = CloudWatchAnnotationSupport.prepareAnnotation(annotationQuery); + expect(preparedAnnotation).toEqual(annotationQuery); + }); + }); + + describe('is being called with legacy annotations', () => { + it('should return a new query', () => { + const preparedAnnotation = CloudWatchAnnotationSupport.prepareAnnotation(legacyAnnotationQuery); + expect(preparedAnnotation).not.toEqual(annotationQuery); + }); + + it('should set default values if not given', () => { + const preparedAnnotation = CloudWatchAnnotationSupport.prepareAnnotation(legacyAnnotationQuery); + expect(preparedAnnotation.target?.statistic).toEqual('Average'); + expect(preparedAnnotation.target?.region).toEqual('default'); + expect(preparedAnnotation.target?.queryMode).toEqual('Annotations'); + expect(preparedAnnotation.target?.refId).toEqual('annotationQuery'); + }); + + it('should not set default values if given', () => { + const annotation = CloudWatchAnnotationSupport.prepareAnnotation({ + ...legacyAnnotationQuery, + statistic: 'Min', + region: 'us-east-2', + queryMode: 'Annotations', + refId: 'A', + }); + expect(annotation.target?.statistic).toEqual('Min'); + expect(annotation.target?.region).toEqual('us-east-2'); + expect(annotation.target?.queryMode).toEqual('Annotations'); + expect(annotation.target?.refId).toEqual('A'); + }); + }); + }); + + describe('when prepareQuery', () => { + describe('is being called without a target', () => { + it('should return undefined', () => { + const preparedQuery = CloudWatchAnnotationSupport.prepareQuery({ + ...annotationQuery, + target: undefined, + }); + expect(preparedQuery).toBeUndefined(); + }); + }); + + describe('is being called with a complete metric stat query', () => { + it('should return the annotation target', () => { + expect(CloudWatchAnnotationSupport.prepareQuery(annotationQuery)).toEqual(annotationQuery.target); + }); + }); + + describe('is being called with an incomplete metric stat query', () => { + it('should return undefined', () => { + const preparedQuery = CloudWatchAnnotationSupport.prepareQuery({ + ...annotationQuery, + target: { + ...annotationQuery.target!, + dimensions: {}, + metricName: '', + statistic: undefined, + }, + }); + expect(preparedQuery).toBeUndefined(); + }); + }); + + describe('is being called with an incomplete prefix matching query', () => { + it('should return the annotation target', () => { + const query = { + ...annotationQuery, + target: prefixMatchingAnnotationQuery, + }; + expect(CloudWatchAnnotationSupport.prepareQuery(query)).toEqual(query.target); + }); + }); + + describe('is being called with an incomplete prefix matching query', () => { + it('should return undefined', () => { + const query = { + ...annotationQuery, + target: { + ...prefixMatchingAnnotationQuery, + actionPrefix: '', + }, + }; + expect(CloudWatchAnnotationSupport.prepareQuery(query)).toBeUndefined(); + }); + }); + }); +}); diff --git a/public/app/plugins/datasource/cloudwatch/annotationSupport.ts b/public/app/plugins/datasource/cloudwatch/annotationSupport.ts new file mode 100644 index 00000000000..88a6c3fe9c7 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/annotationSupport.ts @@ -0,0 +1,60 @@ +import { AnnotationQuery } from '@grafana/data'; + +import { AnnotationQueryEditor } from './components/AnnotationQueryEditor'; +import { isCloudWatchAnnotation } from './guards'; +import { CloudWatchAnnotationQuery, CloudWatchQuery, LegacyAnnotationQuery } from './types'; + +export const CloudWatchAnnotationSupport = { + // converts legacy angular style queries to new format. Also sets the same default values as in the deprecated angular directive + prepareAnnotation: ( + query: LegacyAnnotationQuery | AnnotationQuery + ): AnnotationQuery => { + if (isCloudWatchAnnotation(query)) { + return query; + } + + return { + // setting AnnotationQuery props explicitly since spreading would incorrectly use props that should be on the target only + datasource: query.datasource, + enable: query.enable, + iconColor: query.iconColor, + name: query.name, + builtIn: query.builtIn, + hide: query.hide, + target: { + ...query.target, + ...query, + statistic: query.statistic || 'Average', + region: query.region || 'default', + queryMode: 'Annotations', + refId: query.refId || 'annotationQuery', + }, + }; + }, + // return undefined if query is not complete so that annotation query execution is quietly skipped + prepareQuery: (anno: AnnotationQuery): CloudWatchQuery | undefined => { + if (!anno.target) { + return undefined; + } + + const { + prefixMatching, + actionPrefix, + alarmNamePrefix, + statistic, + namespace, + metricName, + dimensions = {}, + } = anno.target; + const validPrefixMatchingQuery = !!prefixMatching && !!actionPrefix && !!alarmNamePrefix; + const validMetricStatQuery = + !prefixMatching && !!namespace && !!metricName && !!statistic && !!Object.values(dimensions).length; + + if (validPrefixMatchingQuery || validMetricStatQuery) { + return anno.target; + } + + return undefined; + }, + QueryEditor: AnnotationQueryEditor, +}; diff --git a/public/app/plugins/datasource/cloudwatch/annotations_query_ctrl.ts b/public/app/plugins/datasource/cloudwatch/annotations_query_ctrl.ts deleted file mode 100644 index 93ee1f1c19f..00000000000 --- a/public/app/plugins/datasource/cloudwatch/annotations_query_ctrl.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { defaultsDeep } from 'lodash'; - -import { CloudWatchAnnotationQuery } from './types'; - -export class CloudWatchAnnotationsQueryCtrl { - static templateUrl = 'partials/annotations.editor.html'; - declare annotation: any; - - /** @ngInject */ - constructor($scope: any) { - this.annotation = $scope.ctrl.annotation; - - defaultsDeep(this.annotation, { - namespace: '', - metricName: '', - expression: '', - dimensions: {}, - region: 'default', - id: '', - alias: '', - statistic: 'Average', - matchExact: true, - prefixMatching: false, - actionPrefix: '', - alarmNamePrefix: '', - }); - - this.onChange = this.onChange.bind(this); - } - - onChange(query: CloudWatchAnnotationQuery) { - Object.assign(this.annotation, query); - } -} diff --git a/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.test.tsx index be67800951f..de5fa83daae 100644 --- a/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.test.tsx @@ -1,8 +1,12 @@ import { render, screen, waitFor } from '@testing-library/react'; import React from 'react'; +import '@testing-library/jest-dom'; + +import { QueryEditorProps } from '@grafana/data'; import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; -import { CloudWatchAnnotationQuery } from '../types'; +import { CloudWatchDatasource } from '../datasource'; +import { CloudWatchAnnotationQuery, CloudWatchJsonData, CloudWatchMetricsQuery, CloudWatchQuery } from '../types'; import { AnnotationQueryEditor } from './AnnotationQueryEditor'; @@ -10,21 +14,16 @@ const ds = setupMockedDataSource({ variables: [], }); -const q: CloudWatchAnnotationQuery = { - id: '', +const q: CloudWatchQuery = { + queryMode: 'Annotations', region: 'us-east-2', namespace: '', period: '', - alias: '', metricName: '', dimensions: {}, matchExact: true, statistic: '', - expression: '', refId: '', - enable: true, - name: '', - iconColor: '', prefixMatching: false, actionPrefix: '', alarmNamePrefix: '', @@ -36,7 +35,7 @@ ds.datasource.getMetrics = jest.fn().mockResolvedValue([]); ds.datasource.getDimensionKeys = jest.fn().mockResolvedValue([]); ds.datasource.getVariables = jest.fn().mockReturnValue([]); -const props = { +const props: QueryEditorProps = { datasource: ds.datasource, query: q, onChange: jest.fn(), @@ -51,11 +50,18 @@ describe('AnnotationQueryEditor', () => { }); }); + it('should return an error component in case CloudWatchQuery is not CloudWatchAnnotationQuery', async () => { + ds.datasource.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); + render( + + ); + await waitFor(() => expect(screen.getByText('Invalid annotation query')).toBeInTheDocument()); + }); + it('should not display wildcard option in dimension value dropdown', async () => { ds.datasource.getDimensionValues = jest.fn().mockResolvedValue([[{ label: 'dimVal1', value: 'dimVal1' }]]); - props.query.dimensions = { instanceId: 'instance-123' }; + (props.query as CloudWatchAnnotationQuery).dimensions = { instanceId: 'instance-123' }; render(); - const valueElement = screen.getByText('instance-123'); expect(valueElement).toBeInTheDocument(); expect(screen.queryByText('*')).toBeNull(); diff --git a/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.tsx index 84660850dc7..c2992d87365 100644 --- a/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/AnnotationQueryEditor.tsx @@ -1,27 +1,30 @@ import React, { ChangeEvent } from 'react'; -import { PanelData } from '@grafana/data'; -import { EditorField, EditorHeader, EditorRow, EditorSwitch, InlineSelect, Space } from '@grafana/experimental'; -import { Input } from '@grafana/ui'; +import { QueryEditorProps } from '@grafana/data'; +import { EditorField, EditorHeader, EditorRow, InlineSelect, Space, EditorSwitch } from '@grafana/experimental'; +import { Alert, Input } from '@grafana/ui'; import { CloudWatchDatasource } from '../datasource'; +import { isCloudWatchAnnotationQuery } from '../guards'; import { useRegions } from '../hooks'; -import { CloudWatchAnnotationQuery, CloudWatchMetricsQuery } from '../types'; +import { CloudWatchJsonData, CloudWatchQuery, MetricStat } from '../types'; import { MetricStatEditor } from './MetricStatEditor'; -export type Props = { - query: CloudWatchAnnotationQuery; - datasource: CloudWatchDatasource; - onChange: (value: CloudWatchAnnotationQuery) => void; - data?: PanelData; -}; +export type Props = QueryEditorProps; -export function AnnotationQueryEditor(props: React.PropsWithChildren) { +export const AnnotationQueryEditor = (props: Props) => { const { query, onChange, datasource } = props; - const [regions, regionIsLoading] = useRegions(datasource); + if (!isCloudWatchAnnotationQuery(query)) { + return ( + + {JSON.stringify(query, null, 4)} + + ); + } + return ( <> @@ -38,8 +41,10 @@ export function AnnotationQueryEditor(props: React.PropsWithChildren) { onChange({ ...query, ...editorQuery })} + onChange={(metricStat: MetricStat) => onChange({ ...query, ...metricStat })} onRunQuery={() => {}} > @@ -81,4 +86,4 @@ export function AnnotationQueryEditor(props: React.PropsWithChildren) { ); -} +}; diff --git a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx index 3cd88220ee6..61fb326b562 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.test.tsx @@ -43,7 +43,7 @@ describe('Dimensions', () => { InstanceId: '*', InstanceGroup: 'Group1', }; - render(); + render(); const filterItems = screen.getAllByTestId('cloudwatch-dimensions-filter-item'); expect(filterItems.length).toBe(2); @@ -59,7 +59,7 @@ describe('Dimensions', () => { it('it should add the new item but not call onChange', async () => { props.query.dimensions = {}; const onChange = jest.fn(); - render(); + render(); await userEvent.click(screen.getByLabelText('Add')); expect(screen.getByTestId('cloudwatch-dimensions-filter-item')).toBeInTheDocument(); @@ -72,7 +72,7 @@ describe('Dimensions', () => { props.query.dimensions = {}; const onChange = jest.fn(); const { container } = render( - + ); await userEvent.click(screen.getByLabelText('Add')); @@ -92,7 +92,7 @@ describe('Dimensions', () => { props.query.dimensions = {}; const onChange = jest.fn(); const { container } = render( - + ); const label = await screen.findByLabelText('Add'); diff --git a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.tsx b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.tsx index a016b35ae83..44a0f08887c 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/Dimensions/Dimensions.tsx @@ -1,16 +1,16 @@ import { isEqual } from 'lodash'; -import React, { useEffect, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { SelectableValue } from '@grafana/data'; import { EditorList } from '@grafana/experimental'; import { CloudWatchDatasource } from '../../datasource'; -import { Dimensions as DimensionsType, DimensionsQuery } from '../../types'; +import { Dimensions as DimensionsType, MetricStat } from '../../types'; import { FilterItem } from './FilterItem'; export interface Props { - query: DimensionsQuery; + metricStat: MetricStat; onChange: (dimensions: DimensionsType) => void; datasource: CloudWatchDatasource; dimensionKeys: Array>; @@ -45,16 +45,22 @@ const filterConditionsToDimensions = (filters: DimensionFilterCondition[]) => { }, {}); }; -export const Dimensions: React.FC = ({ query, datasource, dimensionKeys, disableExpressions, onChange }) => { - const [items, setItems] = useState([]); - useEffect(() => setItems(dimensionsToFilterConditions(query.dimensions)), [query.dimensions]); +export const Dimensions: React.FC = ({ + metricStat, + datasource, + dimensionKeys, + disableExpressions, + onChange, +}) => { + const dimensionFilters = useMemo(() => dimensionsToFilterConditions(metricStat.dimensions), [metricStat.dimensions]); + const [items, setItems] = useState(dimensionFilters); const onDimensionsChange = (newItems: Array>) => { setItems(newItems); // The onChange event should only be triggered in the case there is a complete dimension object. // So when a new key is added that does not yet have a value, it should not trigger an onChange event. const newDimensions = filterConditionsToDimensions(newItems); - if (!isEqual(newDimensions, query.dimensions)) { + if (!isEqual(newDimensions, metricStat.dimensions)) { onChange(newDimensions); } }; @@ -63,14 +69,14 @@ export const Dimensions: React.FC = ({ query, datasource, dimensionKeys, ); }; function makeRenderFilter( datasource: CloudWatchDatasource, - query: DimensionsQuery, + metricStat: MetricStat, dimensionKeys: Array>, disableExpressions: boolean ) { @@ -84,7 +90,7 @@ function makeRenderFilter( filter={item} onChange={(item) => onChange(item)} datasource={datasource} - query={query} + metricStat={metricStat} disableExpressions={disableExpressions} dimensionKeys={dimensionKeys} onDelete={onDelete} diff --git a/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx b/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx index 9ffc5847392..8fb947ba310 100644 --- a/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/Dimensions/FilterItem.tsx @@ -3,17 +3,17 @@ import React, { FunctionComponent, useMemo } from 'react'; import { useAsyncFn } from 'react-use'; import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data'; -import { InputGroup, AccessoryButton } from '@grafana/experimental'; +import { AccessoryButton, InputGroup } from '@grafana/experimental'; import { Select, stylesFactory, useTheme2 } from '@grafana/ui'; import { CloudWatchDatasource } from '../../datasource'; -import { Dimensions, DimensionsQuery } from '../../types'; +import { Dimensions, MetricStat } from '../../types'; import { appendTemplateVariables } from '../../utils/utils'; import { DimensionFilterCondition } from './Dimensions'; export interface Props { - query: DimensionsQuery; + metricStat: MetricStat; datasource: CloudWatchDatasource; filter: DimensionFilterCondition; dimensionKeys: Array>; @@ -34,7 +34,7 @@ const excludeCurrentKey = (dimensions: Dimensions, currentKey: string | undefine export const FilterItem: FunctionComponent = ({ filter, - query: { region, namespace, metricName, dimensions }, + metricStat: { region, namespace, metricName, dimensions }, datasource, dimensionKeys, disableExpressions, diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx index 050de9a2324..87be4afbfaa 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { intersectionBy, debounce, unionBy } from 'lodash'; +import { debounce, intersectionBy, unionBy } from 'lodash'; import { LanguageMap, languages as prismLanguages } from 'prismjs'; import React, { ReactNode } from 'react'; import { Editor, Node, Plugin } from 'slate'; @@ -19,6 +19,8 @@ import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; import { dispatch } from 'app/store/store'; import { ExploreId } from 'app/types'; +// Utils & Services +// dom also includes Element polyfills import { CloudWatchDatasource } from '../datasource'; import { CloudWatchLanguageProvider } from '../language_provider'; @@ -339,7 +341,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent { await userEvent.type(statisticElement, statistic); fireEvent.keyDown(statisticElement, { keyCode: 13 }); - expect(onChange).toHaveBeenCalledWith({ ...props.query, statistic }); + expect(onChange).toHaveBeenCalledWith({ ...props.metricStat, statistic }); expect(onRunQuery).toHaveBeenCalled(); }); @@ -96,7 +92,13 @@ describe('MetricStatEditor', () => { }); it('should be unchecked when value is false', async () => { - render(); + render( + + ); expect(await screen.findByLabelText('Match exact - optional')).not.toBeChecked(); }); }); @@ -139,24 +141,24 @@ describe('MetricStatEditor', () => { await selectEvent.select(metricsSelect, 'm1'); expect(onChange.mock.calls).toEqual([ - [{ ...propsNamespaceMetrics.query, namespace: 'n1' }], // First call, namespace select - [{ ...propsNamespaceMetrics.query, metricName: 'm1' }], // Second call, metric select + [{ ...propsNamespaceMetrics.metricStat, namespace: 'n1' }], // First call, namespace select + [{ ...propsNamespaceMetrics.metricStat, metricName: 'm1' }], // Second call, metric select ]); expect(onRunQuery).toHaveBeenCalledTimes(2); }); - it('should remove metricName from query if it does not exist in new namespace', async () => { + it('should remove metricName from metricStat if it does not exist in new namespace', async () => { propsNamespaceMetrics.datasource.getMetrics = jest .fn() .mockImplementation((namespace: string, region: string) => { let mockMetrics = - namespace === 'n1' && region === props.query.region + namespace === 'n1' && region === props.metricStat.region ? metrics : [{ value: 'oldNamespaceMetric', label: 'oldNamespaceMetric', text: 'oldNamespaceMetric' }]; return Promise.resolve(mockMetrics); }); - propsNamespaceMetrics.query.metricName = 'oldNamespaceMetric'; - propsNamespaceMetrics.query.namespace = 'n2'; + propsNamespaceMetrics.metricStat.metricName = 'oldNamespaceMetric'; + propsNamespaceMetrics.metricStat.namespace = 'n2'; await act(async () => { render(); @@ -167,12 +169,12 @@ describe('MetricStatEditor', () => { await selectEvent.select(namespaceSelect, 'n1'); - expect(onChange.mock.calls).toEqual([[{ ...propsNamespaceMetrics.query, metricName: '', namespace: 'n1' }]]); + expect(onChange.mock.calls).toEqual([[{ ...propsNamespaceMetrics.metricStat, metricName: '', namespace: 'n1' }]]); }); - it('should not remove metricName from query if it does exist in new namespace', async () => { - propsNamespaceMetrics.query.namespace = 'n1'; - propsNamespaceMetrics.query.metricName = 'm1'; + it('should not remove metricName from metricStat if it does exist in new namespace', async () => { + propsNamespaceMetrics.metricStat.namespace = 'n1'; + propsNamespaceMetrics.metricStat.metricName = 'm1'; await act(async () => { render(); @@ -184,7 +186,9 @@ describe('MetricStatEditor', () => { await selectEvent.select(namespaceSelect, 'n2'); expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange.mock.calls).toEqual([[{ ...propsNamespaceMetrics.query, metricName: 'm1', namespace: 'n2' }]]); + expect(onChange.mock.calls).toEqual([ + [{ ...propsNamespaceMetrics.metricStat, metricName: 'm1', namespace: 'n2' }], + ]); }); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.tsx index db61ea5cd8d..c27dd5aab07 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricStatEditor/MetricStatEditor.tsx @@ -7,50 +7,52 @@ import { Select } from '@grafana/ui'; import { Dimensions } from '..'; import { CloudWatchDatasource } from '../../datasource'; import { useDimensionKeys, useMetrics, useNamespaces } from '../../hooks'; -import { CloudWatchMetricsQuery } from '../../types'; +import { MetricStat } from '../../types'; import { appendTemplateVariables, toOption } from '../../utils/utils'; export type Props = { - query: CloudWatchMetricsQuery; + refId: string; + metricStat: MetricStat; datasource: CloudWatchDatasource; disableExpressions?: boolean; - onChange: (value: CloudWatchMetricsQuery) => void; + onChange: (value: MetricStat) => void; onRunQuery: () => void; }; export function MetricStatEditor({ - query, + refId, + metricStat, datasource, disableExpressions = false, onChange, onRunQuery, }: React.PropsWithChildren) { - const { region, namespace, metricName, dimensions } = query; + const { region, namespace, metricName, dimensions } = metricStat; const namespaces = useNamespaces(datasource); const metrics = useMetrics(datasource, region, namespace); const dimensionKeys = useDimensionKeys(datasource, region, namespace, metricName, dimensions ?? {}); - const onQueryChange = (query: CloudWatchMetricsQuery) => { - onChange(query); + const onMetricStatChange = (metricStat: MetricStat) => { + onChange(metricStat); onRunQuery(); }; - const onNamespaceChange = async (query: CloudWatchMetricsQuery) => { - const validatedQuery = await validateMetricName(query); - onQueryChange(validatedQuery); + const onNamespaceChange = async (metricStat: MetricStat) => { + const validatedQuery = await validateMetricName(metricStat); + onMetricStatChange(validatedQuery); }; - const validateMetricName = async (query: CloudWatchMetricsQuery) => { - let { metricName, namespace, region } = query; + const validateMetricName = async (metricStat: MetricStat) => { + let { metricName, namespace, region } = metricStat; if (!metricName) { - return query; + return metricStat; } await datasource.getMetrics(namespace, region).then((result: Array>) => { if (!result.find((metric) => metric.value === metricName)) { metricName = ''; } }); - return { ...query, metricName }; + return { ...metricStat, metricName }; }; return ( @@ -60,12 +62,12 @@ export function MetricStatEditor({ { if (metricName) { - onQueryChange({ ...query, metricName }); + onMetricStatChange({ ...metricStat, metricName }); } }} /> @@ -86,12 +88,12 @@ export function MetricStatEditor({ - -
- - -
- - -
-
- - -
-
- - - - Alias replacement variables: -
    -
  • {{ metric }}
  • -
  • {{ stat }}
  • -
  • {{ namespace }}
  • -
  • {{ region }}
  • -
  • {{ period }}
  • -
  • {{ label }}
  • -
  • {{ YOUR_DIMENSION_NAME }}
  • -
-
-
- -
-
-
-
diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index 72dfae2fe70..df2004957bc 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -579,9 +579,9 @@ describe('CloudWatchDatasource', () => { ds.interpolateVariablesInQueries([logQuery], {}); - // We interpolate `expression` and `region` in CloudWatchLogsQuery + // We interpolate `region` in CloudWatchLogsQuery expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); - expect(templateSrv.replace).toHaveBeenCalledTimes(2); + expect(templateSrv.replace).toHaveBeenCalledTimes(1); }); it('should replace correct variables in CloudWatchMetricsQuery', () => { @@ -589,9 +589,9 @@ describe('CloudWatchDatasource', () => { const { ds } = getTestContext({ templateSrv }); const variableName = 'someVar'; const logQuery: CloudWatchMetricsQuery = { + queryMode: 'Metrics', id: 'someId', refId: 'someRefId', - queryMode: 'Metrics', expression: `$${variableName}`, region: `$${variableName}`, period: `$${variableName}`, @@ -610,7 +610,7 @@ describe('CloudWatchDatasource', () => { // We interpolate `expression`, `region`, `period`, `alias`, `metricName`, `nameSpace` and `dimensions` in CloudWatchMetricsQuery expect(templateSrv.replace).toHaveBeenCalledWith(`$${variableName}`, {}); - expect(templateSrv.replace).toHaveBeenCalledTimes(9); + expect(templateSrv.replace).toHaveBeenCalledTimes(8); }); }); diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts index 0380c8a1d78..38462070b93 100644 --- a/public/app/plugins/datasource/cloudwatch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/types.ts @@ -1,17 +1,17 @@ -import { AwsAuthDataSourceSecureJsonData, AwsAuthDataSourceJsonData } from '@grafana/aws-sdk'; +import { AwsAuthDataSourceJsonData, AwsAuthDataSourceSecureJsonData } from '@grafana/aws-sdk'; import { DataQuery, DataSourceRef, SelectableValue } from '@grafana/data'; -export interface Dimensions { - [key: string]: string | string[]; -} - import { QueryEditorArrayExpression, QueryEditorFunctionExpression, QueryEditorPropertyExpression, } from './expressions'; -export type CloudWatchQueryMode = 'Metrics' | 'Logs'; +export interface Dimensions { + [key: string]: string | string[]; +} + +export type CloudWatchQueryMode = 'Metrics' | 'Logs' | 'Annotations'; export enum MetricQueryType { 'Search', @@ -35,43 +35,36 @@ export interface SQLExpression { limit?: number; } -export interface DimensionsQuery extends DataQuery { - namespace: string; - region: string; - metricName?: string; - dimensions?: Dimensions; -} - -export interface CloudWatchMetricsQuery extends DataQuery { +export interface CloudWatchMetricsQuery extends MetricStat, DataQuery { queryMode?: 'Metrics'; metricQueryType?: MetricQueryType; metricEditorMode?: MetricEditorMode; //common props id: string; - region: string; - namespace: string; - period?: string; alias?: string; - //Basic editor builder props - metricName?: string; - dimensions?: Dimensions; - matchExact?: boolean; - statistic?: string; - /** - * @deprecated use statistic - */ - statistics?: string[]; - // Math expression query expression?: string; sqlExpression?: string; - sql?: SQLExpression; } +export interface MetricStat { + region: string; + namespace: string; + metricName?: string; + dimensions?: Dimensions; + matchExact?: boolean; + period?: string; + statistic?: string; + /** + * @deprecated use statistic + */ + statistics?: string[]; +} + export interface CloudWatchMathExpressionQuery extends DataQuery { expression: string; } @@ -95,7 +88,6 @@ export enum CloudWatchLogsQueryStatus { export interface CloudWatchLogsQuery extends DataQuery { queryMode: 'Logs'; - id: string; region: string; expression?: string; @@ -103,22 +95,15 @@ export interface CloudWatchLogsQuery extends DataQuery { statsGroups?: string[]; } -export type CloudWatchQuery = CloudWatchMetricsQuery | CloudWatchLogsQuery; +export type CloudWatchQuery = CloudWatchMetricsQuery | CloudWatchLogsQuery | CloudWatchAnnotationQuery; -export const isCloudWatchLogsQuery = (cloudwatchQuery: CloudWatchQuery): cloudwatchQuery is CloudWatchLogsQuery => - (cloudwatchQuery as CloudWatchLogsQuery).queryMode === 'Logs'; - -interface AnnotationProperties { - enable: boolean; - name: string; - iconColor: string; - prefixMatching: boolean; - actionPrefix: string; - alarmNamePrefix: string; +export interface CloudWatchAnnotationQuery extends MetricStat, DataQuery { + queryMode: 'Annotations'; + prefixMatching?: boolean; + actionPrefix?: string; + alarmNamePrefix?: string; } -export type CloudWatchAnnotationQuery = CloudWatchMetricsQuery & AnnotationProperties; - export type SelectableStrings = Array>; export interface CloudWatchJsonData extends AwsAuthDataSourceJsonData { @@ -369,7 +354,7 @@ export interface MetricRequest { export interface MetricQuery { [key: string]: any; - datasource: DataSourceRef; + datasource?: DataSourceRef; refId?: string; maxDataPoints?: number; intervalMs?: number; @@ -400,3 +385,33 @@ export interface VariableQuery extends DataQuery { resourceType: string; tags: string; } + +export interface LegacyAnnotationQuery extends MetricStat, DataQuery { + actionPrefix: string; + alarmNamePrefix: string; + alias: string; + builtIn: number; + datasource: any; + dimensions: Dimensions; + enable: boolean; + expression: string; + hide: boolean; + iconColor: string; + id: string; + matchExact: boolean; + metricName: string; + name: string; + namespace: string; + period: string; + prefixMatching: boolean; + region: string; + statistic: string; + statistics: string[]; + target: { + limit: number; + matchAny: boolean; + tags: any[]; + type: string; + }; + type: string; +}