Cloudwatch: Migrate queries that use multiple stats to one query per stat (#36925)

* migrate queries that use multiple stats - squash commits

* fix typo
This commit is contained in:
Erik Sundell
2021-09-08 16:06:43 +02:00
committed by GitHub
parent ae9343f8ae
commit 5e38b02f94
34 changed files with 2304 additions and 1493 deletions

View File

@@ -2,14 +2,14 @@ import React, { ChangeEvent } from 'react';
import { LegacyForms } from '@grafana/ui';
const { Switch } = LegacyForms;
import { PanelData } from '@grafana/data';
import { AnnotationQuery } from '../types';
import { CloudWatchAnnotationQuery } from '../types';
import { CloudWatchDatasource } from '../datasource';
import { QueryField, PanelQueryEditor } from './';
export type Props = {
query: AnnotationQuery;
query: CloudWatchAnnotationQuery;
datasource: CloudWatchDatasource;
onChange: (value: AnnotationQuery) => void;
onChange: (value: CloudWatchAnnotationQuery) => void;
data?: PanelData;
};
@@ -20,7 +20,7 @@ export function AnnotationQueryEditor(props: React.PropsWithChildren<Props>) {
<>
<PanelQueryEditor
{...props}
onChange={(editorQuery: AnnotationQuery) => onChange({ ...query, ...editorQuery })}
onChange={(editorQuery: CloudWatchAnnotationQuery) => onChange({ ...query, ...editorQuery })}
onRunQuery={() => {}}
history={[]}
></PanelQueryEditor>

View File

@@ -1,10 +1,9 @@
import React, { PureComponent, ChangeEvent } from 'react';
import { isEmpty } from 'lodash';
import { ExploreQueryFieldProps } from '@grafana/data';
import { ExploreQueryFieldProps, PanelData } from '@grafana/data';
import { LegacyForms, ValidationEvents, EventsWithValidation, Icon } from '@grafana/ui';
const { Input, Switch } = LegacyForms;
import { CloudWatchQuery, CloudWatchMetricsQuery, CloudWatchJsonData } from '../types';
import { CloudWatchQuery, CloudWatchMetricsQuery, CloudWatchJsonData, ExecutedQueryPreview } from '../types';
import { CloudWatchDatasource } from '../datasource';
import { QueryField, Alias, MetricsQueryFieldsEditor } from './';
@@ -31,7 +30,7 @@ export const normalizeQuery = ({
region,
id,
alias,
statistics,
statistic,
period,
...rest
}: CloudWatchMetricsQuery): CloudWatchMetricsQuery => {
@@ -43,7 +42,7 @@ export const normalizeQuery = ({
region: region || 'default',
id: id || '',
alias: alias || '',
statistics: isEmpty(statistics) ? ['Average'] : statistics,
statistic: statistic ?? 'Average',
period: period || '',
...rest,
};
@@ -65,55 +64,65 @@ export class MetricsQueryEditor extends PureComponent<Props, State> {
onRunQuery();
}
getExecutedQueryPreview(data?: PanelData): ExecutedQueryPreview {
if (!(data?.series.length && data?.series[0].meta?.custom)) {
return {
executedQuery: '',
period: '',
id: '',
};
}
return {
executedQuery: data?.series[0].meta.executedQueryString ?? '',
period: data.series[0].meta.custom['period'],
id: data.series[0].meta.custom['id'],
};
}
render() {
const { data, onRunQuery } = this.props;
const metricsQuery = this.props.query as CloudWatchMetricsQuery;
const { showMeta } = this.state;
const query = normalizeQuery(metricsQuery);
const executedQueries =
data && data.series.length && data.series[0].meta && data.state === 'Done'
? data.series[0].meta.executedQueryString
: null;
const executedQueryPreview = this.getExecutedQueryPreview(data);
return (
<>
<MetricsQueryFieldsEditor {...{ ...this.props, query }}></MetricsQueryFieldsEditor>
{query.statistics.length <= 1 && (
<div className="gf-form-inline">
<div className="gf-form">
<QueryField
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>) =>
this.onChange({ ...metricsQuery, id: event.target.value })
}
validationEvents={idValidationEvents}
value={query.id}
/>
</QueryField>
</div>
<div className="gf-form gf-form--grow">
<QueryField
className="gf-form--grow"
label="Expression"
tooltip="Optionally you can add an expression here. Please note that if a math expression that is referencing other queries is being used, it will not be possible to create an alert rule based on this query"
>
<Input
className="gf-form-input"
onBlur={onRunQuery}
value={query.expression || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
this.onChange({ ...metricsQuery, expression: event.target.value })
}
/>
</QueryField>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<QueryField
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>) =>
this.onChange({ ...metricsQuery, id: event.target.value })
}
validationEvents={idValidationEvents}
value={query.id}
/>
</QueryField>
</div>
)}
<div className="gf-form gf-form--grow">
<QueryField
className="gf-form--grow"
label="Expression"
tooltip="Optionally you can add an expression here. Please note that if a math expression that is referencing other queries is being used, it will not be possible to create an alert rule based on this query"
>
<Input
className="gf-form-input"
onBlur={onRunQuery}
value={query.expression || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
this.onChange({ ...metricsQuery, expression: event.target.value })
}
/>
</QueryField>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<QueryField label="Period" tooltip="Minimum interval between points in seconds">
@@ -153,21 +162,20 @@ export class MetricsQueryEditor extends PureComponent<Props, State> {
<label className="gf-form-label">
<a
onClick={() =>
executedQueries &&
executedQueryPreview &&
this.setState({
showMeta: !showMeta,
})
}
>
<Icon name={showMeta && executedQueries ? 'angle-down' : 'angle-right'} />{' '}
{showMeta && executedQueries ? 'Hide' : 'Show'} Query Preview
<Icon name={showMeta ? 'angle-down' : 'angle-right'} /> {showMeta ? 'Hide' : 'Show'} Query Preview
</a>
</label>
</div>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--grow" />
</div>
{showMeta && executedQueries && (
{showMeta && (
<table className="filter-table form-inline">
<thead>
<tr>
@@ -178,13 +186,11 @@ export class MetricsQueryEditor extends PureComponent<Props, State> {
</tr>
</thead>
<tbody>
{JSON.parse(executedQueries).map(({ ID, Expression, Period }: any) => (
<tr key={ID}>
<td>{ID}</td>
<td>{Expression}</td>
<td>{Period}</td>
</tr>
))}
<tr>
<td>{executedQueryPreview.id}</td>
<td>{executedQueryPreview.executedQuery}</td>
<td>{executedQueryPreview.period}</td>
</tr>
</tbody>
</table>
)}

View File

@@ -3,7 +3,7 @@ import { SelectableValue } from '@grafana/data';
import { Segment, SegmentAsync } from '@grafana/ui';
import { CloudWatchMetricsQuery, SelectableStrings } from '../types';
import { CloudWatchDatasource } from '../datasource';
import { Dimensions, QueryInlineField, Stats } from '.';
import { Dimensions, QueryInlineField } from '.';
export type Props = {
query: CloudWatchMetricsQuery;
@@ -120,12 +120,25 @@ export function MetricsQueryFieldsEditor({
/>
</QueryInlineField>
<QueryInlineField label="Stats">
<Stats
stats={datasource.standardStatistics.map(toOption)}
values={metricsQuery.statistics}
onChange={(statistics) => onQueryChange({ ...metricsQuery, statistics })}
variableOptionGroup={variableOptionGroup}
<QueryInlineField label="Statistic">
<Segment
allowCustomValue
value={query.statistic}
options={[
...datasource.standardStatistics.filter((s) => s !== query.statistic).map(toOption),
variableOptionGroup,
]}
onChange={({ value: statistic }) => {
if (
!datasource.standardStatistics.includes(statistic) &&
!/^p\d{2}(?:\.\d{1,2})?$/.test(statistic) &&
!statistic.startsWith('$')
) {
return;
}
onQueryChange({ ...metricsQuery, statistic });
}}
/>
</QueryInlineField>

View File

@@ -1,21 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Stats } from './Stats';
const toOption = (value: any) => ({ label: value, value });
describe('Stats', () => {
it('should render component', () => {
render(
<Stats
data-testid="stats"
values={['Average', 'Minimum']}
variableOptionGroup={{ label: 'templateVar', value: 'templateVar' }}
onChange={() => {}}
stats={['Average', 'Maximum', 'Minimum', 'Sum', 'SampleCount'].map(toOption)}
/>
);
expect(screen.getByText('Average')).toBeInTheDocument();
expect(screen.getByText('Minimum')).toBeInTheDocument();
});
});

View File

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

View File

@@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QueryEditor should render component 1`] = `null`;

View File

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