mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -1,3 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`QueryEditor should render component 1`] = `null`;
|
||||
@@ -1,4 +1,3 @@
|
||||
export { Stats } from './Stats';
|
||||
export { Dimensions } from './Dimensions';
|
||||
export { QueryInlineField, QueryField } from './Forms';
|
||||
export { Alias } from './Alias';
|
||||
|
||||
Reference in New Issue
Block a user