mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Cloudwatch: Refactor metrics query editor (#48537)
* refactor metrics query editor to return a function component * add tests for metrics query editor * add simple render tests for panel query editor * remove obsolete test * pr feedback
This commit is contained in:
@@ -49,7 +49,9 @@ export function setupMockedDataSource({
|
|||||||
} as any
|
} as any
|
||||||
);
|
);
|
||||||
datasource.getVariables = () => ['test'];
|
datasource.getVariables = () => ['test'];
|
||||||
datasource.getRegions = () => Promise.resolve([]);
|
|
||||||
|
datasource.getNamespaces = jest.fn().mockResolvedValue([]);
|
||||||
|
datasource.getRegions = jest.fn().mockResolvedValue([]);
|
||||||
const fetchMock = jest.fn().mockReturnValue(of({ data }));
|
const fetchMock = jest.fn().mockReturnValue(of({ data }));
|
||||||
setBackendSrv({ fetch: fetchMock } as any);
|
setBackendSrv({ fetch: fetchMock } as any);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { CustomVariableModel, initialVariableModelState } from '../../../../feat
|
|||||||
import { CloudWatchDatasource } from '../datasource';
|
import { CloudWatchDatasource } from '../datasource';
|
||||||
import { CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../types';
|
import { CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../types';
|
||||||
|
|
||||||
import { MetricsQueryEditor, normalizeQuery, Props } from './MetricsQueryEditor';
|
import { MetricsQueryEditor, Props } from './MetricsQueryEditor';
|
||||||
|
|
||||||
const setup = () => {
|
const setup = () => {
|
||||||
const instanceSettings = {
|
const instanceSettings = {
|
||||||
@@ -79,64 +79,6 @@ describe('QueryEditor', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('normalizes query on mount', async () => {
|
|
||||||
const { act } = renderer;
|
|
||||||
const props = setup();
|
|
||||||
// This does not actually even conform to the prop type but this happens on initialisation somehow
|
|
||||||
props.query = {
|
|
||||||
queryMode: 'Metrics',
|
|
||||||
apiMode: 'Metrics',
|
|
||||||
refId: '',
|
|
||||||
expression: '',
|
|
||||||
matchExact: true,
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
} as any;
|
|
||||||
await act(async () => {
|
|
||||||
renderer.create(<MetricsQueryEditor {...props} />);
|
|
||||||
});
|
|
||||||
expect((props.onChange as jest.Mock).mock.calls[0][0]).toEqual({
|
|
||||||
namespace: '',
|
|
||||||
metricName: '',
|
|
||||||
expression: '',
|
|
||||||
sqlExpression: '',
|
|
||||||
dimensions: {},
|
|
||||||
region: 'default',
|
|
||||||
id: '',
|
|
||||||
alias: '',
|
|
||||||
statistic: 'Average',
|
|
||||||
period: '',
|
|
||||||
queryMode: 'Metrics',
|
|
||||||
apiMode: 'Metrics',
|
|
||||||
refId: '',
|
|
||||||
matchExact: true,
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should use correct default values', () => {
|
|
||||||
it('should normalize query with default values', () => {
|
|
||||||
expect(normalizeQuery({ refId: '42' } as any)).toEqual({
|
|
||||||
namespace: '',
|
|
||||||
metricName: '',
|
|
||||||
expression: '',
|
|
||||||
sqlExpression: '',
|
|
||||||
dimensions: {},
|
|
||||||
region: 'default',
|
|
||||||
id: '',
|
|
||||||
alias: '',
|
|
||||||
statistic: 'Average',
|
|
||||||
matchExact: true,
|
|
||||||
period: '',
|
|
||||||
queryMode: 'Metrics',
|
|
||||||
refId: '42',
|
|
||||||
metricQueryType: MetricQueryType.Search,
|
|
||||||
metricEditorMode: MetricEditorMode.Builder,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should handle editor modes correctly', () => {
|
describe('should handle editor modes correctly', () => {
|
||||||
it('when metric query type is metric search and editor mode is builder', async () => {
|
it('when metric query type is metric search and editor mode is builder', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { ChangeEvent, PureComponent } from 'react';
|
import React, { ChangeEvent, useState } from 'react';
|
||||||
|
|
||||||
import { QueryEditorProps } from '@grafana/data';
|
import { QueryEditorProps } from '@grafana/data';
|
||||||
import { EditorField, EditorRow, Space } from '@grafana/experimental';
|
import { EditorField, EditorRow, Space } from '@grafana/experimental';
|
||||||
@@ -16,181 +16,132 @@ import {
|
|||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
import QueryHeader from './QueryHeader';
|
import QueryHeader from './QueryHeader';
|
||||||
|
import usePreparedMetricsQuery from './usePreparedMetricsQuery';
|
||||||
|
|
||||||
import { Alias, MathExpressionQueryField, MetricStatEditor, SQLBuilderEditor, SQLCodeEditor } from './';
|
import { Alias, MathExpressionQueryField, MetricStatEditor, SQLBuilderEditor, SQLCodeEditor } from './';
|
||||||
|
|
||||||
export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>;
|
export interface Props extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> {
|
||||||
|
query: CloudWatchMetricsQuery;
|
||||||
interface State {
|
|
||||||
sqlCodeEditorIsDirty: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const normalizeQuery = ({
|
export const MetricsQueryEditor = (props: Props) => {
|
||||||
namespace,
|
const { query, onRunQuery, datasource } = props;
|
||||||
metricName,
|
const [sqlCodeEditorIsDirty, setSQLCodeEditorIsDirty] = useState(false);
|
||||||
expression,
|
const preparedQuery = usePreparedMetricsQuery(query, props.onChange);
|
||||||
dimensions,
|
|
||||||
region,
|
|
||||||
id,
|
|
||||||
alias,
|
|
||||||
statistic,
|
|
||||||
period,
|
|
||||||
sqlExpression,
|
|
||||||
metricQueryType,
|
|
||||||
metricEditorMode,
|
|
||||||
...rest
|
|
||||||
}: CloudWatchMetricsQuery): CloudWatchMetricsQuery => {
|
|
||||||
const normalizedQuery = {
|
|
||||||
queryMode: 'Metrics' as const,
|
|
||||||
namespace: namespace ?? '',
|
|
||||||
metricName: metricName ?? '',
|
|
||||||
expression: expression ?? '',
|
|
||||||
dimensions: dimensions ?? {},
|
|
||||||
region: region ?? 'default',
|
|
||||||
id: id ?? '',
|
|
||||||
alias: alias ?? '',
|
|
||||||
statistic: statistic ?? 'Average',
|
|
||||||
period: period ?? '',
|
|
||||||
metricQueryType: metricQueryType ?? MetricQueryType.Search,
|
|
||||||
metricEditorMode: metricEditorMode ?? MetricEditorMode.Builder,
|
|
||||||
sqlExpression: sqlExpression ?? '',
|
|
||||||
...rest,
|
|
||||||
};
|
|
||||||
return !rest.hasOwnProperty('matchExact') ? { ...normalizedQuery, matchExact: true } : normalizedQuery;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class MetricsQueryEditor extends PureComponent<Props, State> {
|
const onChange = (query: CloudWatchQuery) => {
|
||||||
state = {
|
const { onChange, onRunQuery } = props;
|
||||||
sqlCodeEditorIsDirty: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount = () => {
|
|
||||||
const metricsQuery = this.props.query as CloudWatchMetricsQuery;
|
|
||||||
const query = normalizeQuery(metricsQuery);
|
|
||||||
this.props.onChange(query);
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange = (query: CloudWatchQuery) => {
|
|
||||||
const { onChange, onRunQuery } = this.props;
|
|
||||||
onChange(query);
|
onChange(query);
|
||||||
onRunQuery();
|
onRunQuery();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const { onRunQuery, datasource } = this.props;
|
<>
|
||||||
const metricsQuery = this.props.query as CloudWatchMetricsQuery;
|
<QueryHeader
|
||||||
const query = normalizeQuery(metricsQuery);
|
query={query}
|
||||||
|
onRunQuery={onRunQuery}
|
||||||
|
datasource={datasource}
|
||||||
|
onChange={(newQuery) => {
|
||||||
|
if (isCloudWatchMetricsQuery(newQuery) && newQuery.metricEditorMode !== query.metricEditorMode) {
|
||||||
|
setSQLCodeEditorIsDirty(false);
|
||||||
|
}
|
||||||
|
onChange(newQuery);
|
||||||
|
}}
|
||||||
|
sqlCodeEditorIsDirty={sqlCodeEditorIsDirty}
|
||||||
|
/>
|
||||||
|
<Space v={0.5} />
|
||||||
|
|
||||||
return (
|
{query.metricQueryType === MetricQueryType.Search && (
|
||||||
<>
|
<>
|
||||||
<QueryHeader
|
{query.metricEditorMode === MetricEditorMode.Builder && (
|
||||||
query={query}
|
<MetricStatEditor
|
||||||
onRunQuery={onRunQuery}
|
{...props}
|
||||||
datasource={datasource}
|
refId={query.refId}
|
||||||
onChange={(newQuery) => {
|
metricStat={query}
|
||||||
if (isCloudWatchMetricsQuery(newQuery) && newQuery.metricEditorMode !== query.metricEditorMode) {
|
onChange={(metricStat: MetricStat) => props.onChange({ ...query, ...metricStat })}
|
||||||
this.setState({ sqlCodeEditorIsDirty: false });
|
></MetricStatEditor>
|
||||||
|
)}
|
||||||
|
{query.metricEditorMode === MetricEditorMode.Code && (
|
||||||
|
<MathExpressionQueryField
|
||||||
|
onRunQuery={onRunQuery}
|
||||||
|
expression={query.expression ?? ''}
|
||||||
|
onChange={(expression) => props.onChange({ ...query, expression })}
|
||||||
|
datasource={datasource}
|
||||||
|
></MathExpressionQueryField>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{query.metricQueryType === MetricQueryType.Query && (
|
||||||
|
<>
|
||||||
|
{query.metricEditorMode === MetricEditorMode.Code && (
|
||||||
|
<SQLCodeEditor
|
||||||
|
region={query.region}
|
||||||
|
sql={query.sqlExpression ?? ''}
|
||||||
|
onChange={(sqlExpression) => {
|
||||||
|
if (!sqlCodeEditorIsDirty) {
|
||||||
|
setSQLCodeEditorIsDirty(true);
|
||||||
|
}
|
||||||
|
props.onChange({ ...preparedQuery, sqlExpression });
|
||||||
|
}}
|
||||||
|
onRunQuery={onRunQuery}
|
||||||
|
datasource={datasource}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{query.metricEditorMode === MetricEditorMode.Builder && (
|
||||||
|
<>
|
||||||
|
<SQLBuilderEditor
|
||||||
|
query={query}
|
||||||
|
onChange={props.onChange}
|
||||||
|
onRunQuery={onRunQuery}
|
||||||
|
datasource={datasource}
|
||||||
|
></SQLBuilderEditor>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Space v={0.5} />
|
||||||
|
<EditorRow>
|
||||||
|
<EditorField
|
||||||
|
label="ID"
|
||||||
|
width={26}
|
||||||
|
optional
|
||||||
|
tooltip="ID can be used to reference other queries in math expressions. The ID can include numbers, letters, and underscore, and must start with a lowercase letter."
|
||||||
|
invalid={!!query.id && !/^$|^[a-z][a-zA-Z0-9_]*$/.test(query.id)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id={`${query.refId}-cloudwatch-metric-query-editor-id`}
|
||||||
|
onBlur={onRunQuery}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange({ ...preparedQuery, id: event.target.value })}
|
||||||
|
type="text"
|
||||||
|
value={query.id}
|
||||||
|
/>
|
||||||
|
</EditorField>
|
||||||
|
|
||||||
|
<EditorField label="Period" width={26} tooltip="Minimum interval between points in seconds.">
|
||||||
|
<Input
|
||||||
|
id={`${query.refId}-cloudwatch-metric-query-editor-period`}
|
||||||
|
value={query.period || ''}
|
||||||
|
placeholder="auto"
|
||||||
|
onBlur={onRunQuery}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange({ ...preparedQuery, period: event.target.value })
|
||||||
}
|
}
|
||||||
this.onChange(newQuery);
|
/>
|
||||||
}}
|
</EditorField>
|
||||||
sqlCodeEditorIsDirty={this.state.sqlCodeEditorIsDirty}
|
|
||||||
/>
|
|
||||||
<Space v={0.5} />
|
|
||||||
|
|
||||||
{query.metricQueryType === MetricQueryType.Search && (
|
<EditorField
|
||||||
<>
|
label="Alias"
|
||||||
{query.metricEditorMode === MetricEditorMode.Builder && (
|
width={26}
|
||||||
<MetricStatEditor
|
optional
|
||||||
{...this.props}
|
tooltip="Change time series legend name using this field. See documentation for replacement variable formats."
|
||||||
refId={query.refId}
|
>
|
||||||
metricStat={query}
|
<Alias
|
||||||
onChange={(metricStat: MetricStat) => this.props.onChange({ ...query, ...metricStat })}
|
value={preparedQuery.alias ?? ''}
|
||||||
></MetricStatEditor>
|
onChange={(value: string) => onChange({ ...preparedQuery, alias: value })}
|
||||||
)}
|
/>
|
||||||
{query.metricEditorMode === MetricEditorMode.Code && (
|
</EditorField>
|
||||||
<MathExpressionQueryField
|
</EditorRow>
|
||||||
onRunQuery={onRunQuery}
|
</>
|
||||||
expression={query.expression ?? ''}
|
);
|
||||||
onChange={(expression) => this.props.onChange({ ...query, expression })}
|
};
|
||||||
datasource={datasource}
|
|
||||||
></MathExpressionQueryField>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{query.metricQueryType === MetricQueryType.Query && (
|
|
||||||
<>
|
|
||||||
{query.metricEditorMode === MetricEditorMode.Code && (
|
|
||||||
<SQLCodeEditor
|
|
||||||
region={query.region}
|
|
||||||
sql={query.sqlExpression ?? ''}
|
|
||||||
onChange={(sqlExpression) => {
|
|
||||||
if (!this.state.sqlCodeEditorIsDirty) {
|
|
||||||
this.setState({ sqlCodeEditorIsDirty: true });
|
|
||||||
}
|
|
||||||
this.props.onChange({ ...metricsQuery, sqlExpression });
|
|
||||||
}}
|
|
||||||
onRunQuery={onRunQuery}
|
|
||||||
datasource={datasource}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{query.metricEditorMode === MetricEditorMode.Builder && (
|
|
||||||
<>
|
|
||||||
<SQLBuilderEditor
|
|
||||||
query={query}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
onRunQuery={onRunQuery}
|
|
||||||
datasource={datasource}
|
|
||||||
></SQLBuilderEditor>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Space v={0.5} />
|
|
||||||
<EditorRow>
|
|
||||||
<EditorField
|
|
||||||
label="ID"
|
|
||||||
width={26}
|
|
||||||
optional
|
|
||||||
tooltip="ID can be used to reference other queries in math expressions. The ID can include numbers, letters, and underscore, and must start with a lowercase letter."
|
|
||||||
invalid={!!query.id && !/^$|^[a-z][a-zA-Z0-9_]*$/.test(query.id)}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
id={`${query.refId}-cloudwatch-metric-query-editor-id`}
|
|
||||||
onBlur={onRunQuery}
|
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
|
||||||
this.onChange({ ...metricsQuery, id: event.target.value })
|
|
||||||
}
|
|
||||||
type="text"
|
|
||||||
value={query.id}
|
|
||||||
/>
|
|
||||||
</EditorField>
|
|
||||||
|
|
||||||
<EditorField label="Period" width={26} tooltip="Minimum interval between points in seconds.">
|
|
||||||
<Input
|
|
||||||
id={`${query.refId}-cloudwatch-metric-query-editor-period`}
|
|
||||||
value={query.period || ''}
|
|
||||||
placeholder="auto"
|
|
||||||
onBlur={onRunQuery}
|
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
|
||||||
this.onChange({ ...metricsQuery, period: event.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</EditorField>
|
|
||||||
|
|
||||||
<EditorField
|
|
||||||
label="Alias"
|
|
||||||
width={26}
|
|
||||||
optional
|
|
||||||
tooltip="Change time series legend name using this field. See documentation for replacement variable formats."
|
|
||||||
>
|
|
||||||
<Alias
|
|
||||||
value={metricsQuery.alias ?? ''}
|
|
||||||
onChange={(value: string) => this.onChange({ ...metricsQuery, alias: value })}
|
|
||||||
/>
|
|
||||||
</EditorField>
|
|
||||||
</EditorRow>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { act, render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { QueryEditorProps } from '@grafana/data';
|
||||||
|
|
||||||
|
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
|
||||||
|
import { CloudWatchDatasource } from '../datasource';
|
||||||
|
import { CloudWatchQuery, CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../types';
|
||||||
|
|
||||||
|
import { PanelQueryEditor } from './PanelQueryEditor';
|
||||||
|
|
||||||
|
// the following three fields are added to legacy queries in the dashboard migrator
|
||||||
|
const migratedFields = {
|
||||||
|
statistic: 'Average',
|
||||||
|
metricEditorMode: MetricEditorMode.Builder,
|
||||||
|
metricQueryType: MetricQueryType.Query,
|
||||||
|
};
|
||||||
|
|
||||||
|
const props: QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> = {
|
||||||
|
datasource: setupMockedDataSource().datasource,
|
||||||
|
onRunQuery: jest.fn(),
|
||||||
|
onChange: jest.fn(),
|
||||||
|
query: {} as CloudWatchQuery,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('PanelQueryEditor should render right editor', () => {
|
||||||
|
describe('when using grafana 6.3.0 metric query', () => {
|
||||||
|
it('should render the metrics query editor', async () => {
|
||||||
|
const query = {
|
||||||
|
...migratedFields,
|
||||||
|
dimensions: {
|
||||||
|
InstanceId: 'i-123',
|
||||||
|
},
|
||||||
|
expression: '',
|
||||||
|
highResolution: false,
|
||||||
|
id: '',
|
||||||
|
metricName: 'CPUUtilization',
|
||||||
|
namespace: 'AWS/EC2',
|
||||||
|
period: '',
|
||||||
|
refId: 'A',
|
||||||
|
region: 'default',
|
||||||
|
returnData: false,
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
render(<PanelQueryEditor {...props} query={query} />);
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Metric name')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when using grafana 7.0.0 style metrics query', () => {
|
||||||
|
it('should render the metrics query editor', async () => {
|
||||||
|
const query = {
|
||||||
|
...migratedFields,
|
||||||
|
alias: '',
|
||||||
|
apiMode: 'Logs',
|
||||||
|
dimensions: {
|
||||||
|
InstanceId: 'i-123',
|
||||||
|
},
|
||||||
|
expression: '',
|
||||||
|
id: '',
|
||||||
|
logGroupNames: [],
|
||||||
|
matchExact: true,
|
||||||
|
metricName: 'CPUUtilization',
|
||||||
|
namespace: 'AWS/EC2',
|
||||||
|
period: '',
|
||||||
|
queryMode: 'Logs',
|
||||||
|
refId: 'A',
|
||||||
|
region: 'ap-northeast-2',
|
||||||
|
statistics: 'Average',
|
||||||
|
} as any;
|
||||||
|
await act(async () => {
|
||||||
|
render(<PanelQueryEditor {...props} query={query} />);
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Choose Log Groups')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when using grafana 7.0.0 style logs query', () => {
|
||||||
|
it('should render the metrics query editor', async () => {
|
||||||
|
const query = {
|
||||||
|
...migratedFields,
|
||||||
|
alias: '',
|
||||||
|
apiMode: 'Logs',
|
||||||
|
dimensions: {
|
||||||
|
InstanceId: 'i-123',
|
||||||
|
},
|
||||||
|
expression: '',
|
||||||
|
id: '',
|
||||||
|
logGroupNames: [],
|
||||||
|
matchExact: true,
|
||||||
|
metricName: 'CPUUtilization',
|
||||||
|
namespace: 'AWS/EC2',
|
||||||
|
period: '',
|
||||||
|
queryMode: 'Logs',
|
||||||
|
refId: 'A',
|
||||||
|
region: 'ap-northeast-2',
|
||||||
|
statistic: 'Average',
|
||||||
|
} as any;
|
||||||
|
await act(async () => {
|
||||||
|
render(<PanelQueryEditor {...props} query={query} />);
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Log Groups')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when using grafana query from curated ec2 dashboard', () => {
|
||||||
|
it('should render the metrics query editor', async () => {
|
||||||
|
const query = {
|
||||||
|
...migratedFields,
|
||||||
|
|
||||||
|
alias: 'Inbound',
|
||||||
|
dimensions: {
|
||||||
|
InstanceId: '*',
|
||||||
|
},
|
||||||
|
expression:
|
||||||
|
"SUM(REMOVE_EMPTY(SEARCH('{AWS/EC2,InstanceId} MetricName=\"NetworkIn\"', 'Sum', $period)))/$period",
|
||||||
|
id: '',
|
||||||
|
matchExact: true,
|
||||||
|
metricName: 'NetworkOut',
|
||||||
|
namespace: 'AWS/EC2',
|
||||||
|
period: '$period',
|
||||||
|
refId: 'B',
|
||||||
|
region: '$region',
|
||||||
|
statistic: 'Average',
|
||||||
|
} as any;
|
||||||
|
await act(async () => {
|
||||||
|
render(<PanelQueryEditor {...props} query={query} />);
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Metric name')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
import { QueryEditorProps, ExploreMode } from '@grafana/data';
|
import { QueryEditorProps } from '@grafana/data';
|
||||||
|
|
||||||
import { CloudWatchDatasource } from '../datasource';
|
import { CloudWatchDatasource } from '../datasource';
|
||||||
|
import { isCloudWatchMetricsQuery } from '../guards';
|
||||||
import { CloudWatchJsonData, CloudWatchQuery } from '../types';
|
import { CloudWatchJsonData, CloudWatchQuery } from '../types';
|
||||||
|
|
||||||
import LogsQueryEditor from './LogsQueryEditor';
|
import LogsQueryEditor from './LogsQueryEditor';
|
||||||
@@ -13,14 +14,13 @@ export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, Clou
|
|||||||
export class PanelQueryEditor extends PureComponent<Props> {
|
export class PanelQueryEditor extends PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { query } = this.props;
|
const { query } = this.props;
|
||||||
const apiMode = query.queryMode ?? 'Metrics';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{apiMode === ExploreMode.Logs ? (
|
{isCloudWatchMetricsQuery(query) ? (
|
||||||
<LogsQueryEditor {...this.props} allowCustomValue />
|
<MetricsQueryEditor {...this.props} query={query} />
|
||||||
) : (
|
) : (
|
||||||
<MetricsQueryEditor {...this.props} />
|
<LogsQueryEditor {...this.props} allowCustomValue />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
|
||||||
|
import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types';
|
||||||
|
|
||||||
|
import usePreparedMetricsQuery, { DEFAULT_QUERY } from './usePreparedMetricsQuery';
|
||||||
|
|
||||||
|
interface TestScenario {
|
||||||
|
name: string;
|
||||||
|
query: any;
|
||||||
|
expectedQuery: CloudWatchMetricsQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseQuery: CloudWatchMetricsQuery = {
|
||||||
|
refId: 'A',
|
||||||
|
id: '',
|
||||||
|
region: 'us-east-2',
|
||||||
|
namespace: 'AWS/EC2',
|
||||||
|
dimensions: { InstanceId: 'x-123' },
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('usePrepareMetricsQuery', () => {
|
||||||
|
describe('when an incomplete query is provided', () => {
|
||||||
|
const testTable: TestScenario[] = [
|
||||||
|
{ name: 'Empty query', query: { refId: 'A' }, expectedQuery: { ...DEFAULT_QUERY, refId: 'A' } },
|
||||||
|
{
|
||||||
|
name: 'Match exact is not part of the query',
|
||||||
|
query: { ...baseQuery },
|
||||||
|
expectedQuery: { ...DEFAULT_QUERY, ...baseQuery, matchExact: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Match exact is part of the query',
|
||||||
|
query: { ...baseQuery, matchExact: false },
|
||||||
|
expectedQuery: { ...DEFAULT_QUERY, ...baseQuery, matchExact: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'When editor mode and builder mode different from default is specified',
|
||||||
|
query: { ...baseQuery, metricQueryType: MetricQueryType.Query, metricEditorMode: MetricEditorMode.Code },
|
||||||
|
expectedQuery: {
|
||||||
|
...DEFAULT_QUERY,
|
||||||
|
...baseQuery,
|
||||||
|
metricQueryType: MetricQueryType.Query,
|
||||||
|
metricEditorMode: MetricEditorMode.Code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
describe.each(testTable)('scenario %#: $name', (scenario) => {
|
||||||
|
it('should set the default values and trigger onChangeQuery', async () => {
|
||||||
|
const onChangeQuery = jest.fn();
|
||||||
|
const { result } = renderHook(() => usePreparedMetricsQuery(scenario.query, onChangeQuery));
|
||||||
|
expect(onChangeQuery).toHaveBeenLastCalledWith(result.current);
|
||||||
|
expect(result.current).toEqual(scenario.expectedQuery);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when a complete query is provided', () => {
|
||||||
|
it('should not change the query and should not call onChangeQuery', async () => {
|
||||||
|
const onChangeQuery = jest.fn();
|
||||||
|
const completeQuery: CloudWatchMetricsQuery = {
|
||||||
|
...baseQuery,
|
||||||
|
expression: '',
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
metricName: '',
|
||||||
|
statistic: 'Sum',
|
||||||
|
period: '300',
|
||||||
|
metricQueryType: MetricQueryType.Query,
|
||||||
|
metricEditorMode: MetricEditorMode.Code,
|
||||||
|
sqlExpression: 'SELECT 1',
|
||||||
|
matchExact: false,
|
||||||
|
};
|
||||||
|
const { result } = renderHook(() => usePreparedMetricsQuery(completeQuery, onChangeQuery));
|
||||||
|
expect(onChangeQuery).not.toHaveBeenCalled();
|
||||||
|
expect(result.current).toEqual(completeQuery);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import deepEqual from 'fast-deep-equal';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types';
|
||||||
|
|
||||||
|
export const DEFAULT_QUERY: Omit<CloudWatchMetricsQuery, 'refId'> = {
|
||||||
|
queryMode: 'Metrics',
|
||||||
|
namespace: '',
|
||||||
|
metricName: '',
|
||||||
|
expression: '',
|
||||||
|
dimensions: {},
|
||||||
|
region: 'default',
|
||||||
|
id: '',
|
||||||
|
statistic: 'Average',
|
||||||
|
period: '',
|
||||||
|
metricQueryType: MetricQueryType.Search,
|
||||||
|
metricEditorMode: MetricEditorMode.Builder,
|
||||||
|
sqlExpression: '',
|
||||||
|
matchExact: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const prepareQuery = (query: CloudWatchMetricsQuery) => {
|
||||||
|
const withDefaults = { ...DEFAULT_QUERY, ...query };
|
||||||
|
|
||||||
|
// If we didn't make any changes to the object, then return the original object to keep the
|
||||||
|
// identity the same, and not trigger any other useEffects or anything.
|
||||||
|
return deepEqual(withDefaults, query) ? query : withDefaults;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns queries with some defaults + migrations, and calls onChange function to notify if it changes
|
||||||
|
*/
|
||||||
|
const usePreparedMetricsQuery = (
|
||||||
|
query: CloudWatchMetricsQuery,
|
||||||
|
onChangeQuery: (newQuery: CloudWatchMetricsQuery) => void
|
||||||
|
) => {
|
||||||
|
const preparedQuery = useMemo(() => prepareQuery(query), [query]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (preparedQuery !== query) {
|
||||||
|
onChangeQuery(preparedQuery);
|
||||||
|
}
|
||||||
|
}, [preparedQuery, query, onChangeQuery]);
|
||||||
|
|
||||||
|
return preparedQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usePreparedMetricsQuery;
|
||||||
@@ -6,7 +6,7 @@ export const isCloudWatchLogsQuery = (cloudwatchQuery: CloudWatchQuery): cloudwa
|
|||||||
cloudwatchQuery.queryMode === 'Logs';
|
cloudwatchQuery.queryMode === 'Logs';
|
||||||
|
|
||||||
export const isCloudWatchMetricsQuery = (cloudwatchQuery: CloudWatchQuery): cloudwatchQuery is CloudWatchMetricsQuery =>
|
export const isCloudWatchMetricsQuery = (cloudwatchQuery: CloudWatchQuery): cloudwatchQuery is CloudWatchMetricsQuery =>
|
||||||
cloudwatchQuery.queryMode === 'Metrics';
|
cloudwatchQuery.queryMode === 'Metrics' || !cloudwatchQuery.hasOwnProperty('queryMode'); // in early versions of this plugin, queryMode wasn't defined in a CloudWatchMetricsQuery
|
||||||
|
|
||||||
export const isCloudWatchAnnotationQuery = (
|
export const isCloudWatchAnnotationQuery = (
|
||||||
cloudwatchQuery: CloudWatchQuery
|
cloudwatchQuery: CloudWatchQuery
|
||||||
|
|||||||
Reference in New Issue
Block a user