Cloudwatch: Add run query button (#60089)

* refactor header

* split left and right header actions

* cleanup

* fix broken tests

* move MetricsQueryEditor tests to QueryEditor tests

* fix mock imports

* remove no longer used onRunQuery func

* move run queries button

* apply defaults also when changing query type

* remove not used prop
This commit is contained in:
Erik Sundell
2022-12-15 14:29:57 +01:00
committed by GitHub
parent 66c076f24e
commit 6928ad2949
32 changed files with 404 additions and 640 deletions

View File

@@ -5346,8 +5346,7 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:5381": [ "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"]
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
], ],
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx:5381": [ "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "0"],
@@ -5361,14 +5360,11 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"] [0, 0, 0, "Unexpected any. Specify a different type.", "1"]
], ],
"public/app/plugins/datasource/cloudwatch/components/PanelQueryEditor.test.tsx:5381": [ "public/app/plugins/datasource/cloudwatch/components/QueryEditor.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"] [0, 0, 0, "Unexpected any. Specify a different type.", "2"]
], ],
"public/app/plugins/datasource/cloudwatch/components/QueryHeader.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/cloudwatch/datasource.ts:5381": [ "public/app/plugins/datasource/cloudwatch/datasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"] [0, 0, 0, "Unexpected any. Specify a different type.", "0"]
], ],

View File

@@ -38,32 +38,30 @@ export const validMetricSearchBuilderQuery: CloudWatchMetricsQuery = {
}; };
export const validMetricQueryBuilderQuery: CloudWatchMetricsQuery = { export const validMetricQueryBuilderQuery: CloudWatchMetricsQuery = {
id: '',
queryMode: 'Metrics', queryMode: 'Metrics',
region: 'us-east-2', refId: '',
namespace: 'AWS/EC2', id: '',
period: '3000', region: 'us-east-1',
alias: '', namespace: 'ec2',
metricName: 'CPUUtilization', dimensions: { somekey: 'somevalue' },
dimensions: { InstanceId: 'i-123' }, metricQueryType: MetricQueryType.Query,
matchExact: true, metricEditorMode: MetricEditorMode.Builder,
statistic: 'Average',
sql: { sql: {
select: { from: {
type: QueryEditorExpressionType.Function, type: QueryEditorExpressionType.Function,
name: 'AVERAGE', name: 'SCHEMA',
parameters: [ parameters: [
{ {
type: QueryEditorExpressionType.FunctionParameter, type: QueryEditorExpressionType.FunctionParameter,
name: 'CPUUtilization', name: 'AWS/EC2',
},
{
type: QueryEditorExpressionType.FunctionParameter,
name: 'InstanceId',
}, },
], ],
}, },
}, },
refId: 'A',
metricQueryType: MetricQueryType.Query,
metricEditorMode: MetricEditorMode.Builder,
hide: false,
}; };
export const validMetricQueryCodeQuery: CloudWatchMetricsQuery = { export const validMetricQueryCodeQuery: CloudWatchMetricsQuery = {

View File

@@ -45,7 +45,6 @@ export const AnnotationQueryEditor = (props: Props) => {
metricStat={query} metricStat={query}
disableExpressions={true} disableExpressions={true}
onChange={(metricStat: MetricStat) => onChange({ ...query, ...metricStat })} onChange={(metricStat: MetricStat) => onChange({ ...query, ...metricStat })}
onRunQuery={() => {}}
></MetricStatEditor> ></MetricStatEditor>
<Space v={0.5} /> <Space v={0.5} />
<EditorRow> <EditorRow>

View File

@@ -35,7 +35,6 @@ const defaultProps = {
}, },
]), ]),
onChange: jest.fn(), onChange: jest.fn(),
onRunQuery: jest.fn(),
}; };
const originalDebounce = lodash.debounce; const originalDebounce = lodash.debounce;
@@ -178,13 +177,4 @@ describe('CrossAccountLogsQueryField', () => {
}, },
]); ]);
}); });
it('runs the query on close of the modal', async () => {
const onRunQuery = jest.fn();
render(<CrossAccountLogsQueryField {...defaultProps} onRunQuery={onRunQuery} />);
await userEvent.click(screen.getByText('Select Log Groups'));
expect(screen.getByText('Log Group Name')).toBeInTheDocument();
await userEvent.click(screen.getByLabelText('Close dialogue'));
expect(onRunQuery).toBeCalledTimes(1);
});
}); });

View File

@@ -16,7 +16,6 @@ type CrossAccountLogsQueryProps = {
accountOptions: Array<SelectableValue<string>>; accountOptions: Array<SelectableValue<string>>;
fetchLogGroups: (params: Partial<DescribeLogGroupsRequest>) => Promise<SelectableResourceValue[]>; fetchLogGroups: (params: Partial<DescribeLogGroupsRequest>) => Promise<SelectableResourceValue[]>;
onChange: (selectedLogGroups: SelectableResourceValue[]) => void; onChange: (selectedLogGroups: SelectableResourceValue[]) => void;
onRunQuery: () => void;
}; };
export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => { export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => {
@@ -30,7 +29,6 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) =>
const toggleModal = () => { const toggleModal = () => {
setIsModalOpen(!isModalOpen); setIsModalOpen(!isModalOpen);
if (isModalOpen) { if (isModalOpen) {
props.onRunQuery();
} else { } else {
setSelectedLogGroups(props.selectedLogGroups); setSelectedLogGroups(props.selectedLogGroups);
searchFn(searchPhrase, searchAccountId); searchFn(searchPhrase, searchAccountId);

View File

@@ -33,7 +33,6 @@ const props = {
query: q, query: q,
disableExpressions: false, disableExpressions: false,
onChange: jest.fn(), onChange: jest.fn(),
onRunQuery: jest.fn(),
}; };
describe('Dimensions', () => { describe('Dimensions', () => {

View File

@@ -13,12 +13,11 @@ const dynamicLabelsCompletionItemProvider = new DynamicLabelsCompletionItemProvi
export interface Props { export interface Props {
onChange: (query: string) => void; onChange: (query: string) => void;
onRunQuery: () => void;
label: string; label: string;
width: number; width: number;
} }
export function DynamicLabelsField({ label, width, onChange, onRunQuery }: Props) { export function DynamicLabelsField({ label, width, onChange }: Props) {
const theme = useTheme2(); const theme = useTheme2();
const styles = getInputStyles({ theme, width }); const styles = getInputStyles({ theme, width });
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@@ -28,13 +27,12 @@ export function DynamicLabelsField({ label, width, onChange, onRunQuery }: Props
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {
const text = editor.getValue(); const text = editor.getValue();
onChange(text); onChange(text);
onRunQuery();
}); });
const containerDiv = containerRef.current; const containerDiv = containerRef.current;
containerDiv !== null && editor.layout({ width: containerDiv.clientWidth, height: containerDiv.clientHeight }); containerDiv !== null && editor.layout({ width: containerDiv.clientWidth, height: containerDiv.clientHeight });
}, },
[onChange, onRunQuery] [onChange]
); );
return ( return (
@@ -69,7 +67,6 @@ export function DynamicLabelsField({ label, width, onChange, onRunQuery }: Props
onBlur={(value) => { onBlur={(value) => {
if (value !== label) { if (value !== label) {
onChange(value); onChange(value);
onRunQuery();
} }
}} }}
onBeforeEditorMount={(monaco: Monaco) => onBeforeEditorMount={(monaco: Monaco) =>

View File

@@ -22,7 +22,6 @@ const defaultProps = {
refId: '', refId: '',
} as CloudWatchLogsQuery, } as CloudWatchLogsQuery,
onChange: jest.fn(), onChange: jest.fn(),
onRunQuery: jest.fn(),
}; };
describe('LogGroupSelection', () => { describe('LogGroupSelection', () => {
beforeEach(() => { beforeEach(() => {

View File

@@ -16,14 +16,13 @@ type Props = {
datasource: CloudWatchDatasource; datasource: CloudWatchDatasource;
query: CloudWatchLogsQuery; query: CloudWatchLogsQuery;
onChange: (value: CloudWatchQuery) => void; onChange: (value: CloudWatchQuery) => void;
onRunQuery: () => void;
}; };
const rowGap = css` const rowGap = css`
gap: 3px; gap: 3px;
`; `;
export const LogGroupSelection = ({ datasource, query, onChange, onRunQuery }: Props) => { export const LogGroupSelection = ({ datasource, query, onChange }: Props) => {
const accountState = useAccountOptions(datasource.api, query.region); const accountState = useAccountOptions(datasource.api, query.region);
return ( return (
@@ -37,7 +36,6 @@ export const LogGroupSelection = ({ datasource, query, onChange, onRunQuery }: P
onChange({ ...query, logGroups: selectedLogGroups, logGroupNames: [] }); onChange({ ...query, logGroups: selectedLogGroups, logGroupNames: [] });
}} }}
accountOptions={accountState.value} accountOptions={accountState.value}
onRunQuery={onRunQuery}
selectedLogGroups={query.logGroups ?? []} /* todo handle defaults */ selectedLogGroups={query.logGroups ?? []} /* todo handle defaults */
/> />
) : ( ) : (
@@ -53,7 +51,6 @@ export const LogGroupSelection = ({ datasource, query, onChange, onRunQuery }: P
onChange={function (logGroupNames: string[]): void { onChange={function (logGroupNames: string[]): void {
onChange({ ...query, logGroupNames, logGroups: [] }); onChange({ ...query, logGroupNames, logGroups: [] });
}} }}
onRunQuery={onRunQuery}
refId={query.refId} refId={query.refId}
/> />
} }

View File

@@ -21,7 +21,6 @@ export interface LogGroupSelectorProps {
onChange: (logGroups: string[]) => void; onChange: (logGroups: string[]) => void;
datasource?: CloudWatchDatasource; datasource?: CloudWatchDatasource;
onRunQuery?: () => void;
onOpenMenu?: () => Promise<void>; onOpenMenu?: () => Promise<void>;
refId?: string; refId?: string;
width?: number | 'auto'; width?: number | 'auto';
@@ -33,7 +32,6 @@ export const LogGroupSelector: React.FC<LogGroupSelectorProps> = ({
selectedLogGroups, selectedLogGroups,
onChange, onChange,
datasource, datasource,
onRunQuery,
onOpenMenu, onOpenMenu,
refId, refId,
width, width,
@@ -135,7 +133,6 @@ export const LogGroupSelector: React.FC<LogGroupSelectorProps> = ({
options={datasource ? appendTemplateVariables(datasource, logGroupOptions) : logGroupOptions} options={datasource ? appendTemplateVariables(datasource, logGroupOptions) : logGroupOptions}
value={selectedLogGroups} value={selectedLogGroups}
onChange={(v) => onChange(v.filter(({ value }) => value).map(({ value }) => value))} onChange={(v) => onChange(v.filter(({ value }) => value).map(({ value }) => value))}
onBlur={onRunQuery}
closeMenuOnSelect={false} closeMenuOnSelect={false}
isClearable isClearable
isOptionDisabled={() => selectedLogGroups.length >= MAX_LOG_GROUPS} isOptionDisabled={() => selectedLogGroups.length >= MAX_LOG_GROUPS}

View File

@@ -2,7 +2,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { memo } from 'react'; import React, { memo } from 'react';
// Types
import { AbsoluteTimeRange, QueryEditorProps } from '@grafana/data'; import { AbsoluteTimeRange, QueryEditorProps } from '@grafana/data';
import { InlineFormLabel } from '@grafana/ui'; import { InlineFormLabel } from '@grafana/ui';
@@ -22,7 +21,7 @@ const labelClass = css`
`; `;
export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor(props: Props) { export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor(props: Props) {
const { query, data, datasource, onRunQuery, onChange, exploreId } = props; const { query, data, datasource, exploreId } = props;
let absolute: AbsoluteTimeRange; let absolute: AbsoluteTimeRange;
if (data?.request?.range?.from) { if (data?.request?.range?.from) {
@@ -40,13 +39,9 @@ export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor
return ( return (
<CloudWatchLogsQueryField <CloudWatchLogsQueryField
{...props}
exploreId={exploreId} exploreId={exploreId}
datasource={datasource}
query={query}
onChange={onChange}
onRunQuery={onRunQuery}
history={[]} history={[]}
data={data}
absoluteRange={absolute} absoluteRange={absolute}
ExtraFieldElement={ ExtraFieldElement={
<InlineFormLabel className={`gf-form-label--btn ${labelClass}`} width="auto" tooltip="Link to Graph in AWS"> <InlineFormLabel className={`gf-form-label--btn ${labelClass}`} width="auto" tooltip="Link to Graph in AWS">

View File

@@ -1,7 +1,6 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import _, { DebouncedFunc } from 'lodash'; // eslint-disable-line lodash/import-scope import _, { DebouncedFunc } from 'lodash'; // eslint-disable-line lodash/import-scope
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils';
import { ExploreId } from '../../../../types'; import { ExploreId } from '../../../../types';
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
@@ -13,28 +12,6 @@ jest
.mockImplementation((func: (...args: any) => any, wait?: number) => func as DebouncedFunc<typeof func>); .mockImplementation((func: (...args: any) => any, wait?: number) => func as DebouncedFunc<typeof func>);
describe('CloudWatchLogsQueryField', () => { describe('CloudWatchLogsQueryField', () => {
it('runs onRunQuery on blur of Log Groups', async () => {
const onRunQuery = jest.fn();
const ds = setupMockedDataSource();
render(
<CloudWatchLogsQueryField
absoluteRange={{ from: 1, to: 10 }}
exploreId={ExploreId.left}
datasource={ds.datasource}
query={{} as any}
onRunQuery={onRunQuery}
onChange={() => {}}
/>
);
const multiSelect = screen.getByLabelText('Log Groups');
await act(async () => {
fireEvent.blur(multiSelect);
});
expect(onRunQuery).toHaveBeenCalled();
});
it('loads defaultLogGroups', async () => { it('loads defaultLogGroups', async () => {
const onRunQuery = jest.fn(); const onRunQuery = jest.fn();
const ds = setupMockedDataSource(); const ds = setupMockedDataSource();

View File

@@ -24,7 +24,6 @@ import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../typ
import { getStatsGroups } from '../utils/query/getStatsGroups'; import { getStatsGroups } from '../utils/query/getStatsGroups';
import { LogGroupSelection } from './LogGroupSelection'; import { LogGroupSelection } from './LogGroupSelection';
import QueryHeader from './QueryHeader';
export interface CloudWatchLogsQueryFieldProps export interface CloudWatchLogsQueryFieldProps
extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>, extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>,
@@ -46,7 +45,7 @@ const plugins: Array<Plugin<Editor>> = [
), ),
]; ];
export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) => { export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) => {
const { query, datasource, onChange, onRunQuery, ExtraFieldElement, data } = props; const { query, datasource, onChange, ExtraFieldElement, data } = props;
const showError = data?.error?.refId === query.refId; const showError = data?.error?.refId === query.refId;
const cleanText = datasource.languageProvider.cleanText; const cleanText = datasource.languageProvider.cleanText;
@@ -85,21 +84,13 @@ export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) =
return ( return (
<> <>
<QueryHeader <LogGroupSelection datasource={datasource} query={query} onChange={onChange} />
query={query}
onRunQuery={onRunQuery}
datasource={datasource}
onChange={onChange}
sqlCodeEditorIsDirty={false}
/>
<LogGroupSelection datasource={datasource} query={query} onChange={onChange} onRunQuery={onRunQuery} />
<div className="gf-form-inline gf-form-inline--nowrap flex-grow-1"> <div className="gf-form-inline gf-form-inline--nowrap flex-grow-1">
<div className="gf-form gf-form--grow flex-shrink-1"> <div className="gf-form gf-form--grow flex-shrink-1">
<QueryField <QueryField
additionalPlugins={plugins} additionalPlugins={plugins}
query={query.expression ?? ''} query={query.expression ?? ''}
onChange={onChangeQuery} onChange={onChangeQuery}
onRunQuery={props.onRunQuery}
onTypeahead={onTypeahead} onTypeahead={onTypeahead}
cleanText={cleanText} cleanText={cleanText}
placeholder="Enter a CloudWatch Logs Insights query (run with Shift+Enter)" placeholder="Enter a CloudWatch Logs Insights query (run with Shift+Enter)"

View File

@@ -10,17 +10,11 @@ import { registerLanguage } from '../monarch/register';
export interface Props { export interface Props {
onChange: (query: string) => void; onChange: (query: string) => void;
onRunQuery: () => void;
expression: string; expression: string;
datasource: CloudWatchDatasource; datasource: CloudWatchDatasource;
} }
export function MathExpressionQueryField({ export function MathExpressionQueryField({ expression: query, onChange, datasource }: React.PropsWithChildren<Props>) {
expression: query,
onChange,
onRunQuery,
datasource,
}: React.PropsWithChildren<Props>) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const onEditorMount = useCallback( const onEditorMount = useCallback(
(editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => { (editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => {
@@ -28,7 +22,6 @@ export function MathExpressionQueryField({
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {
const text = editor.getValue(); const text = editor.getValue();
onChange(text); onChange(text);
onRunQuery();
}); });
// auto resizes the editor to be the height of the content it holds // auto resizes the editor to be the height of the content it holds
@@ -48,7 +41,7 @@ export function MathExpressionQueryField({
editor.onDidContentSizeChange(updateElementHeight); editor.onDidContentSizeChange(updateElementHeight);
updateElementHeight(); updateElementHeight();
}, },
[onChange, onRunQuery] [onChange]
); );
return ( return (
@@ -77,7 +70,6 @@ export function MathExpressionQueryField({
onBlur={(value) => { onBlur={(value) => {
if (value !== query) { if (value !== query) {
onChange(value); onChange(value);
onRunQuery();
} }
}} }}
onBeforeEditorMount={(monaco: Monaco) => onBeforeEditorMount={(monaco: Monaco) =>

View File

@@ -33,7 +33,6 @@ const props = {
datasource: ds.datasource, datasource: ds.datasource,
metricStat, metricStat,
onChange: jest.fn(), onChange: jest.fn(),
onRunQuery: jest.fn(),
}; };
describe('MetricStatEditor', () => { describe('MetricStatEditor', () => {
@@ -43,10 +42,9 @@ describe('MetricStatEditor', () => {
describe('statistics field', () => { describe('statistics field', () => {
test.each([['Average', 'p23.23', 'p34', '$statistic']])('should accept valid values', async (statistic) => { test.each([['Average', 'p23.23', 'p34', '$statistic']])('should accept valid values', async (statistic) => {
const onChange = jest.fn(); const onChange = jest.fn();
const onRunQuery = jest.fn();
props.datasource.getVariables = jest.fn().mockReturnValue(['$statistic']); props.datasource.getVariables = jest.fn().mockReturnValue(['$statistic']);
render(<MetricStatEditor {...props} onChange={onChange} onRunQuery={onRunQuery} />); render(<MetricStatEditor {...props} onChange={onChange} />);
const statisticElement = await screen.findByLabelText('Statistic'); const statisticElement = await screen.findByLabelText('Statistic');
expect(statisticElement).toBeInTheDocument(); expect(statisticElement).toBeInTheDocument();
@@ -54,14 +52,12 @@ describe('MetricStatEditor', () => {
await userEvent.type(statisticElement, statistic); await userEvent.type(statisticElement, statistic);
fireEvent.keyDown(statisticElement, { keyCode: 13 }); fireEvent.keyDown(statisticElement, { keyCode: 13 });
expect(onChange).toHaveBeenCalledWith({ ...props.metricStat, statistic }); expect(onChange).toHaveBeenCalledWith({ ...props.metricStat, statistic });
expect(onRunQuery).toHaveBeenCalled();
}); });
test.each([['CustomStat', 'p23,23', '$statistic']])('should not accept invalid values', async (statistic) => { test.each([['CustomStat', 'p23,23', '$statistic']])('should not accept invalid values', async (statistic) => {
const onChange = jest.fn(); const onChange = jest.fn();
const onRunQuery = jest.fn();
render(<MetricStatEditor {...props} onChange={onChange} onRunQuery={onRunQuery} />); render(<MetricStatEditor {...props} onChange={onChange} />);
const statisticElement = await screen.findByLabelText('Statistic'); const statisticElement = await screen.findByLabelText('Statistic');
expect(statisticElement).toBeInTheDocument(); expect(statisticElement).toBeInTheDocument();
@@ -69,7 +65,6 @@ describe('MetricStatEditor', () => {
await userEvent.type(statisticElement, statistic); await userEvent.type(statisticElement, statistic);
fireEvent.keyDown(statisticElement, { keyCode: 13 }); fireEvent.keyDown(statisticElement, { keyCode: 13 });
expect(onChange).not.toHaveBeenCalled(); expect(onChange).not.toHaveBeenCalled();
expect(onRunQuery).not.toHaveBeenCalled();
}); });
}); });
@@ -120,18 +115,15 @@ describe('MetricStatEditor', () => {
{ value: 'm2', label: 'm2', text: 'm2' }, { value: 'm2', label: 'm2', text: 'm2' },
]; ];
const onChange = jest.fn(); const onChange = jest.fn();
const onRunQuery = jest.fn();
const propsNamespaceMetrics = { const propsNamespaceMetrics = {
...props, ...props,
onChange, onChange,
onRunQuery,
}; };
beforeEach(() => { beforeEach(() => {
propsNamespaceMetrics.datasource.api.getNamespaces = jest.fn().mockResolvedValue(namespaces); propsNamespaceMetrics.datasource.api.getNamespaces = jest.fn().mockResolvedValue(namespaces);
propsNamespaceMetrics.datasource.api.getMetrics = jest.fn().mockResolvedValue(metrics); propsNamespaceMetrics.datasource.api.getMetrics = jest.fn().mockResolvedValue(metrics);
onChange.mockClear(); onChange.mockClear();
onRunQuery.mockClear();
}); });
it('should select namespace and metric name correctly', async () => { it('should select namespace and metric name correctly', async () => {
@@ -151,7 +143,6 @@ describe('MetricStatEditor', () => {
[{ ...propsNamespaceMetrics.metricStat, namespace: 'n1' }], // First call, namespace select [{ ...propsNamespaceMetrics.metricStat, namespace: 'n1' }], // First call, namespace select
[{ ...propsNamespaceMetrics.metricStat, metricName: 'm1' }], // Second call, metric select [{ ...propsNamespaceMetrics.metricStat, metricName: 'm1' }], // Second call, metric select
]); ]);
expect(onRunQuery).toHaveBeenCalledTimes(2);
}); });
it('should remove metricName from metricStat if it does not exist in new namespace', async () => { it('should remove metricName from metricStat if it does not exist in new namespace', async () => {

View File

@@ -19,7 +19,6 @@ export type Props = {
datasource: CloudWatchDatasource; datasource: CloudWatchDatasource;
disableExpressions?: boolean; disableExpressions?: boolean;
onChange: (value: MetricStat) => void; onChange: (value: MetricStat) => void;
onRunQuery: () => void;
}; };
export function MetricStatEditor({ export function MetricStatEditor({
@@ -28,7 +27,6 @@ export function MetricStatEditor({
datasource, datasource,
disableExpressions = false, disableExpressions = false,
onChange, onChange,
onRunQuery,
}: React.PropsWithChildren<Props>) { }: React.PropsWithChildren<Props>) {
const namespaces = useNamespaces(datasource); const namespaces = useNamespaces(datasource);
const metrics = useMetrics(datasource, metricStat); const metrics = useMetrics(datasource, metricStat);
@@ -47,14 +45,9 @@ export function MetricStatEditor({
}); });
}, [accountState, metricStat, onChange, datasource.api]); }, [accountState, metricStat, onChange, datasource.api]);
const onMetricStatChange = (metricStat: MetricStat) => {
onChange(metricStat);
onRunQuery();
};
const onNamespaceChange = async (metricStat: MetricStat) => { const onNamespaceChange = async (metricStat: MetricStat) => {
const validatedQuery = await validateMetricName(metricStat); const validatedQuery = await validateMetricName(metricStat);
onMetricStatChange(validatedQuery); onChange(validatedQuery);
}; };
const validateMetricName = async (metricStat: MetricStat) => { const validateMetricName = async (metricStat: MetricStat) => {
@@ -78,7 +71,6 @@ export function MetricStatEditor({
accountId={metricStat.accountId} accountId={metricStat.accountId}
onChange={(accountId?: string) => { onChange={(accountId?: string) => {
onChange({ ...metricStat, accountId }); onChange({ ...metricStat, accountId });
onRunQuery();
}} }}
accountOptions={accountState?.value || []} accountOptions={accountState?.value || []}
></Account> ></Account>
@@ -105,7 +97,7 @@ export function MetricStatEditor({
options={metrics} options={metrics}
onChange={({ value: metricName }) => { onChange={({ value: metricName }) => {
if (metricName) { if (metricName) {
onMetricStatChange({ ...metricStat, metricName }); onChange({ ...metricStat, metricName });
} }
}} }}
/> />
@@ -130,7 +122,7 @@ export function MetricStatEditor({
return; return;
} }
onMetricStatChange({ ...metricStat, statistic }); onChange({ ...metricStat, statistic });
}} }}
/> />
</EditorField> </EditorField>
@@ -141,7 +133,7 @@ export function MetricStatEditor({
<EditorField label="Dimensions"> <EditorField label="Dimensions">
<Dimensions <Dimensions
metricStat={metricStat} metricStat={metricStat}
onChange={(dimensions) => onMetricStatChange({ ...metricStat, dimensions })} onChange={(dimensions) => onChange({ ...metricStat, dimensions })}
dimensionKeys={dimensionKeys} dimensionKeys={dimensionKeys}
disableExpressions={disableExpressions} disableExpressions={disableExpressions}
datasource={datasource} datasource={datasource}
@@ -157,7 +149,7 @@ export function MetricStatEditor({
id={`${refId}-cloudwatch-match-exact`} id={`${refId}-cloudwatch-match-exact`}
value={!!metricStat.matchExact} value={!!metricStat.matchExact}
onChange={(e) => { onChange={(e) => {
onMetricStatChange({ onChange({
...metricStat, ...metricStat,
matchExact: e.currentTarget.checked, matchExact: e.currentTarget.checked,
}); });

View File

@@ -69,6 +69,8 @@ const setup = () => {
metricQueryType: MetricQueryType.Search, metricQueryType: MetricQueryType.Search,
metricEditorMode: MetricEditorMode.Builder, metricEditorMode: MetricEditorMode.Builder,
}, },
extraHeaderElementLeft: () => {},
extraHeaderElementRight: () => {},
datasource, datasource,
history: [], history: [],
onChange: jest.fn(), onChange: jest.fn(),
@@ -79,66 +81,6 @@ const setup = () => {
}; };
describe('QueryEditor', () => { describe('QueryEditor', () => {
describe('should handle editor modes correctly', () => {
it('when metric query type is metric search and editor mode is builder', async () => {
await act(async () => {
const props = setup();
render(<MetricsQueryEditor {...props} />);
expect(screen.getByText('Metric Search')).toBeInTheDocument();
const radio = screen.getByLabelText('Builder');
expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy();
});
});
it('when metric query type is metric search and editor mode is raw', async () => {
await act(async () => {
const props = setup();
if (props.query.queryMode !== 'Metrics') {
fail(`expected props.query.queryMode to be 'Metrics', got '${props.query.queryMode}' instead`);
}
props.query.metricEditorMode = MetricEditorMode.Code;
render(<MetricsQueryEditor {...props} />);
expect(screen.getByText('Metric Search')).toBeInTheDocument();
const radio = screen.getByLabelText('Code');
expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy();
});
});
it('when metric query type is metric query and editor mode is builder', async () => {
await act(async () => {
const props = setup();
if (props.query.queryMode !== 'Metrics') {
fail(`expected props.query.queryMode to be 'Metrics', got '${props.query.queryMode}' instead`);
}
props.query.metricQueryType = MetricQueryType.Query;
props.query.metricEditorMode = MetricEditorMode.Builder;
render(<MetricsQueryEditor {...props} />);
expect(screen.getByText('Metric Query')).toBeInTheDocument();
const radio = screen.getByLabelText('Builder');
expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy();
});
});
it('when metric query type is metric query and editor mode is raw', async () => {
await act(async () => {
const props = setup();
if (props.query.queryMode !== 'Metrics') {
fail(`expected props.query.queryMode to be 'Metrics', got '${props.query.queryMode}' instead`);
}
props.query.metricQueryType = MetricQueryType.Query;
props.query.metricEditorMode = MetricEditorMode.Code;
render(<MetricsQueryEditor {...props} />);
expect(screen.getByText('Metric Query')).toBeInTheDocument();
const radio = screen.getByLabelText('Code');
expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy();
});
});
});
describe('should handle expression options correctly', () => { describe('should handle expression options correctly', () => {
it('should display match exact switch', async () => { it('should display match exact switch', async () => {
const props = setup(); const props = setup();

View File

@@ -1,13 +1,12 @@
import React, { ChangeEvent, useState } from 'react'; import React, { ChangeEvent, useCallback, useEffect, useState } from 'react';
import { QueryEditorProps } from '@grafana/data'; import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { EditorField, EditorRow, Space } from '@grafana/experimental'; import { EditorField, EditorRow, InlineSelect, Space } from '@grafana/experimental';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { Input } from '@grafana/ui'; import { ConfirmModal, Input, RadioButtonGroup } from '@grafana/ui';
import { MathExpressionQueryField, MetricStatEditor, SQLBuilderEditor, SQLCodeEditor } from '../'; import { MathExpressionQueryField, MetricStatEditor, SQLBuilderEditor } from '../';
import { CloudWatchDatasource } from '../../datasource'; import { CloudWatchDatasource } from '../../datasource';
import { isCloudWatchMetricsQuery } from '../../guards';
import useMigratedMetricsQuery from '../../migrations/useMigratedMetricsQuery'; import useMigratedMetricsQuery from '../../migrations/useMigratedMetricsQuery';
import { import {
CloudWatchJsonData, CloudWatchJsonData,
@@ -18,39 +17,99 @@ import {
MetricStat, MetricStat,
} from '../../types'; } from '../../types';
import { DynamicLabelsField } from '../DynamicLabelsField'; import { DynamicLabelsField } from '../DynamicLabelsField';
import QueryHeader from '../QueryHeader'; import { SQLCodeEditor } from '../SQLCodeEditor';
import { Alias } from './Alias'; import { Alias } from './Alias';
export interface Props extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> { export interface Props extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> {
query: CloudWatchMetricsQuery; query: CloudWatchMetricsQuery;
extraHeaderElementLeft?: React.Dispatch<JSX.Element | undefined>;
extraHeaderElementRight?: React.Dispatch<JSX.Element | undefined>;
} }
const metricEditorModes: Array<SelectableValue<MetricQueryType>> = [
{ label: 'Metric Search', value: MetricQueryType.Search },
{ label: 'Metric Query', value: MetricQueryType.Query },
];
const editorModes = [
{ label: 'Builder', value: MetricEditorMode.Builder },
{ label: 'Code', value: MetricEditorMode.Code },
];
export const MetricsQueryEditor = (props: Props) => { export const MetricsQueryEditor = (props: Props) => {
const { query, onRunQuery, datasource } = props; const { query, datasource, extraHeaderElementLeft, extraHeaderElementRight, onChange } = props;
const [showConfirm, setShowConfirm] = useState(false);
const [sqlCodeEditorIsDirty, setSQLCodeEditorIsDirty] = useState(false); const [sqlCodeEditorIsDirty, setSQLCodeEditorIsDirty] = useState(false);
const migratedQuery = useMigratedMetricsQuery(query, props.onChange); const migratedQuery = useMigratedMetricsQuery(query, props.onChange);
const onChange = (query: CloudWatchQuery) => { const onEditorModeChange = useCallback(
const { onChange, onRunQuery } = props; (newMetricEditorMode: MetricEditorMode) => {
onChange(query); if (
onRunQuery(); sqlCodeEditorIsDirty &&
}; query.metricQueryType === MetricQueryType.Query &&
query.metricEditorMode === MetricEditorMode.Code
) {
setShowConfirm(true);
return;
}
onChange({ ...query, metricEditorMode: newMetricEditorMode });
},
[setShowConfirm, onChange, sqlCodeEditorIsDirty, query]
);
useEffect(() => {
extraHeaderElementLeft?.(
<InlineSelect
aria-label="Metric editor mode"
value={metricEditorModes.find((m) => m.value === query.metricQueryType)}
options={metricEditorModes}
onChange={({ value }) => {
onChange({ ...query, metricQueryType: value });
}}
/>
);
extraHeaderElementRight?.(
<>
<RadioButtonGroup
options={editorModes}
size="sm"
value={query.metricEditorMode}
onChange={onEditorModeChange}
/>
<ConfirmModal
isOpen={showConfirm}
title="Are you sure?"
body="You will lose manual changes done to the query if you go back to the visual builder."
confirmText="Yes, I am sure."
dismissText="No, continue editing the query manually."
icon="exclamation-triangle"
onConfirm={() => {
setShowConfirm(false);
onChange({ ...query, metricEditorMode: MetricEditorMode.Builder });
}}
onDismiss={() => setShowConfirm(false)}
/>
</>
);
return () => {
extraHeaderElementLeft?.(undefined);
extraHeaderElementRight?.(undefined);
};
}, [
query,
sqlCodeEditorIsDirty,
datasource,
onChange,
extraHeaderElementLeft,
extraHeaderElementRight,
showConfirm,
onEditorModeChange,
]);
return ( return (
<> <>
<QueryHeader
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} /> <Space v={0.5} />
{query.metricQueryType === MetricQueryType.Search && ( {query.metricQueryType === MetricQueryType.Search && (
@@ -65,7 +124,6 @@ export const MetricsQueryEditor = (props: Props) => {
)} )}
{query.metricEditorMode === MetricEditorMode.Code && ( {query.metricEditorMode === MetricEditorMode.Code && (
<MathExpressionQueryField <MathExpressionQueryField
onRunQuery={onRunQuery}
expression={query.expression ?? ''} expression={query.expression ?? ''}
onChange={(expression) => props.onChange({ ...query, expression })} onChange={(expression) => props.onChange({ ...query, expression })}
datasource={datasource} datasource={datasource}
@@ -85,19 +143,13 @@ export const MetricsQueryEditor = (props: Props) => {
} }
props.onChange({ ...migratedQuery, sqlExpression }); props.onChange({ ...migratedQuery, sqlExpression });
}} }}
onRunQuery={onRunQuery}
datasource={datasource} datasource={datasource}
/> />
)} )}
{query.metricEditorMode === MetricEditorMode.Builder && ( {query.metricEditorMode === MetricEditorMode.Builder && (
<> <>
<SQLBuilderEditor <SQLBuilderEditor query={query} onChange={props.onChange} datasource={datasource}></SQLBuilderEditor>
query={query}
onChange={props.onChange}
onRunQuery={onRunQuery}
datasource={datasource}
></SQLBuilderEditor>
</> </>
)} )}
</> </>
@@ -113,7 +165,6 @@ export const MetricsQueryEditor = (props: Props) => {
> >
<Input <Input
id={`${query.refId}-cloudwatch-metric-query-editor-id`} id={`${query.refId}-cloudwatch-metric-query-editor-id`}
onBlur={onRunQuery}
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange({ ...migratedQuery, id: event.target.value })} onChange={(event: ChangeEvent<HTMLInputElement>) => onChange({ ...migratedQuery, id: event.target.value })}
type="text" type="text"
value={query.id} value={query.id}
@@ -125,7 +176,6 @@ export const MetricsQueryEditor = (props: Props) => {
id={`${query.refId}-cloudwatch-metric-query-editor-period`} id={`${query.refId}-cloudwatch-metric-query-editor-period`}
value={query.period || ''} value={query.period || ''}
placeholder="auto" placeholder="auto"
onBlur={onRunQuery}
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange={(event: ChangeEvent<HTMLInputElement>) =>
onChange({ ...migratedQuery, period: event.target.value }) onChange({ ...migratedQuery, period: event.target.value })
} }
@@ -141,7 +191,6 @@ export const MetricsQueryEditor = (props: Props) => {
> >
<DynamicLabelsField <DynamicLabelsField
width={52} width={52}
onRunQuery={onRunQuery}
label={migratedQuery.label ?? ''} label={migratedQuery.label ?? ''}
onChange={(label) => props.onChange({ ...query, label })} onChange={(label) => props.onChange({ ...query, label })}
></DynamicLabelsField> ></DynamicLabelsField>

View File

@@ -1,139 +0,0 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { act } from 'react-dom/test-utils';
import { setupMockedDataSource } from '../../__mocks__/CloudWatchDataSource';
import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../../types';
import MetricsQueryHeader from './MetricsQueryHeader';
const ds = setupMockedDataSource({
variables: [],
});
ds.datasource.api.getRegions = jest.fn().mockResolvedValue([]);
const query: CloudWatchMetricsQuery = {
id: '',
region: 'us-east-2',
namespace: '',
period: '',
alias: '',
metricName: '',
dimensions: {},
matchExact: true,
statistic: '',
expression: '',
refId: '',
};
describe('MetricsQueryHeader', () => {
describe('confirm modal', () => {
it('should be shown when moving from code editor to builder when in sql mode', async () => {
const onChange = jest.fn();
const onRunQuery = jest.fn();
query.metricEditorMode = MetricEditorMode.Code;
query.metricQueryType = MetricQueryType.Query;
render(
<MetricsQueryHeader
sqlCodeEditorIsDirty={true}
datasource={ds.datasource}
query={query}
onChange={onChange}
onRunQuery={onRunQuery}
isMonitoringAccount={false}
/>
);
const builderElement = screen.getByLabelText('Builder');
expect(builderElement).toBeInTheDocument();
await act(async () => {
await builderElement.click();
});
const modalTitleElem = screen.getByText('Are you sure?');
expect(modalTitleElem).toBeInTheDocument();
expect(onChange).not.toHaveBeenCalled();
});
it('should not be shown when moving from builder to code when in sql mode', async () => {
const onChange = jest.fn();
const onRunQuery = jest.fn();
query.metricEditorMode = MetricEditorMode.Builder;
query.metricQueryType = MetricQueryType.Query;
render(
<MetricsQueryHeader
sqlCodeEditorIsDirty={true}
datasource={ds.datasource}
query={query}
onChange={onChange}
onRunQuery={onRunQuery}
isMonitoringAccount={false}
/>
);
const builderElement = screen.getByLabelText('Code');
expect(builderElement).toBeInTheDocument();
await act(async () => {
await builderElement.click();
});
const modalTitleElem = screen.queryByText('Are you sure?');
expect(modalTitleElem).toBeNull();
expect(onChange).toHaveBeenCalled();
});
it('should not be shown when moving from code to builder when in standard mode', async () => {
const onChange = jest.fn();
const onRunQuery = jest.fn();
query.metricEditorMode = MetricEditorMode.Code;
query.metricQueryType = MetricQueryType.Search;
render(
<MetricsQueryHeader
sqlCodeEditorIsDirty={true}
datasource={ds.datasource}
query={query}
onChange={onChange}
onRunQuery={onRunQuery}
isMonitoringAccount={false}
/>
);
const builderElement = screen.getByLabelText('Builder');
expect(builderElement).toBeInTheDocument();
await act(async () => {
await builderElement.click();
});
const modalTitleElem = screen.queryByText('Are you sure?');
expect(modalTitleElem).toBeNull();
expect(onChange).toHaveBeenCalled();
});
});
it('should call run query when run button is clicked when in metric query mode', async () => {
const onChange = jest.fn();
const onRunQuery = jest.fn();
query.metricEditorMode = MetricEditorMode.Code;
query.metricQueryType = MetricQueryType.Query;
render(
<MetricsQueryHeader
sqlCodeEditorIsDirty={true}
datasource={ds.datasource}
query={query}
onChange={onChange}
onRunQuery={onRunQuery}
isMonitoringAccount={false}
/>
);
const runQueryButton = screen.getByText('Run query');
expect(runQueryButton).toBeInTheDocument();
await act(async () => {
await runQueryButton.click();
});
expect(onRunQuery).toHaveBeenCalled();
});
});

View File

@@ -1,105 +0,0 @@
import React, { useCallback, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { FlexItem, InlineSelect } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { Badge, Button, ConfirmModal, RadioButtonGroup } from '@grafana/ui';
import { CloudWatchDatasource } from '../../datasource';
import { CloudWatchMetricsQuery, CloudWatchQuery, MetricEditorMode, MetricQueryType } from '../../types';
interface MetricsQueryHeaderProps {
query: CloudWatchMetricsQuery;
datasource: CloudWatchDatasource;
onChange: (query: CloudWatchQuery) => void;
onRunQuery: () => void;
sqlCodeEditorIsDirty: boolean;
isMonitoringAccount: boolean;
}
const metricEditorModes: Array<SelectableValue<MetricQueryType>> = [
{ label: 'Metric Search', value: MetricQueryType.Search },
{ label: 'Metric Query', value: MetricQueryType.Query },
];
const editorModes = [
{ label: 'Builder', value: MetricEditorMode.Builder },
{ label: 'Code', value: MetricEditorMode.Code },
];
const MetricsQueryHeader: React.FC<MetricsQueryHeaderProps> = ({
query,
sqlCodeEditorIsDirty,
onChange,
onRunQuery,
isMonitoringAccount,
}) => {
const { metricEditorMode, metricQueryType } = query;
const [showConfirm, setShowConfirm] = useState(false);
const onEditorModeChange = useCallback(
(newMetricEditorMode: MetricEditorMode) => {
if (
sqlCodeEditorIsDirty &&
metricQueryType === MetricQueryType.Query &&
metricEditorMode === MetricEditorMode.Code
) {
setShowConfirm(true);
return;
}
onChange({ ...query, metricEditorMode: newMetricEditorMode });
},
[setShowConfirm, onChange, sqlCodeEditorIsDirty, query, metricEditorMode, metricQueryType]
);
const shouldDisplayMonitoringBadge =
query.metricQueryType === MetricQueryType.Search &&
isMonitoringAccount &&
config.featureToggles.cloudWatchCrossAccountQuerying;
return (
<>
<InlineSelect
aria-label="Metric editor mode"
value={metricEditorModes.find((m) => m.value === metricQueryType)}
options={metricEditorModes}
onChange={({ value }) => {
onChange({ ...query, metricQueryType: value });
}}
/>
<FlexItem grow={1} />
{shouldDisplayMonitoringBadge && (
<Badge
text="Monitoring account"
color="blue"
tooltip="AWS monitoring accounts view data from source accounts so you can centralize monitoring and troubleshoot activites"
></Badge>
)}
<RadioButtonGroup options={editorModes} size="sm" value={metricEditorMode} onChange={onEditorModeChange} />
{query.metricQueryType === MetricQueryType.Query && query.metricEditorMode === MetricEditorMode.Code && (
<Button variant="secondary" size="sm" onClick={() => onRunQuery()}>
Run query
</Button>
)}
<ConfirmModal
isOpen={showConfirm}
title="Are you sure?"
body="You will lose manual changes done to the query if you go back to the visual builder."
confirmText="Yes, I am sure."
dismissText="No, continue editing the query manually."
icon="exclamation-triangle"
onConfirm={() => {
setShowConfirm(false);
onChange({ ...query, metricEditorMode: MetricEditorMode.Builder });
}}
onDismiss={() => setShowConfirm(false)}
/>
</>
);
};
export default MetricsQueryHeader;

View File

@@ -1,25 +0,0 @@
import React, { PureComponent } from 'react';
import { QueryEditorProps } from '@grafana/data';
import { CloudWatchDatasource } from '../datasource';
import { isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from '../guards';
import { CloudWatchJsonData, CloudWatchQuery } from '../types';
import { MetricsQueryEditor } from '././MetricsQueryEditor/MetricsQueryEditor';
import LogsQueryEditor from './LogsQueryEditor';
export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>;
export class PanelQueryEditor extends PureComponent<Props> {
render() {
const { query } = this.props;
return (
<>
{isCloudWatchMetricsQuery(query) && <MetricsQueryEditor {...this.props} query={query} />}
{isCloudWatchLogsQuery(query) && <LogsQueryEditor {...this.props} query={query} />}
</>
);
}
}

View File

@@ -1,4 +1,4 @@
import { act, render, screen } from '@testing-library/react'; import { act, fireEvent, render, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { QueryEditorProps } from '@grafana/data'; import { QueryEditorProps } from '@grafana/data';
@@ -15,7 +15,7 @@ import {
import { CloudWatchDatasource } from '../datasource'; import { CloudWatchDatasource } from '../datasource';
import { CloudWatchQuery, CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../types'; import { CloudWatchQuery, CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../types';
import { PanelQueryEditor } from './PanelQueryEditor'; import { QueryEditor } from './QueryEditor';
// the following three fields are added to legacy queries in the dashboard migrator // the following three fields are added to legacy queries in the dashboard migrator
const migratedFields = { const migratedFields = {
@@ -31,7 +31,22 @@ const props: QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJ
query: {} as CloudWatchQuery, query: {} as CloudWatchQuery,
}; };
describe('PanelQueryEditor should render right editor', () => { const FAKE_EDITOR_LABEL = 'FakeEditor';
jest.mock('./SQLCodeEditor', () => ({
SQLCodeEditor: ({ sql, onChange }: { sql: string; onChange: (val: string) => void }) => {
return (
<>
<label htmlFor="cloudwatch-fake-editor">{FAKE_EDITOR_LABEL}</label>
<input id="cloudwatch-fake-editor" value={sql} onChange={(e) => onChange(e.currentTarget.value)}></input>
</>
);
},
}));
export { SQLCodeEditor } from './SQLCodeEditor';
describe('QueryEditor should render right editor', () => {
describe('when using grafana 6.3.0 metric query', () => { describe('when using grafana 6.3.0 metric query', () => {
it('should render the metrics query editor', async () => { it('should render the metrics query editor', async () => {
const query = { const query = {
@@ -50,7 +65,7 @@ describe('PanelQueryEditor should render right editor', () => {
returnData: false, returnData: false,
}; };
await act(async () => { await act(async () => {
render(<PanelQueryEditor {...props} query={query} />); render(<QueryEditor {...props} query={query} />);
}); });
expect(screen.getByText('Metric name')).toBeInTheDocument(); expect(screen.getByText('Metric name')).toBeInTheDocument();
}); });
@@ -78,7 +93,7 @@ describe('PanelQueryEditor should render right editor', () => {
statistics: 'Average', statistics: 'Average',
} as any; } as any;
await act(async () => { await act(async () => {
render(<PanelQueryEditor {...props} query={query} />); render(<QueryEditor {...props} query={query} />);
}); });
expect(screen.getByText('Choose Log Groups')).toBeInTheDocument(); expect(screen.getByText('Choose Log Groups')).toBeInTheDocument();
}); });
@@ -106,7 +121,7 @@ describe('PanelQueryEditor should render right editor', () => {
statistic: 'Average', statistic: 'Average',
} as any; } as any;
await act(async () => { await act(async () => {
render(<PanelQueryEditor {...props} query={query} />); render(<QueryEditor {...props} query={query} />);
}); });
expect(screen.getByText('Log Groups')).toBeInTheDocument(); expect(screen.getByText('Log Groups')).toBeInTheDocument();
}); });
@@ -133,7 +148,7 @@ describe('PanelQueryEditor should render right editor', () => {
statistic: 'Average', statistic: 'Average',
} as any; } as any;
await act(async () => { await act(async () => {
render(<PanelQueryEditor {...props} query={query} />); render(<QueryEditor {...props} query={query} />);
}); });
expect(screen.getByText('Metric name')).toBeInTheDocument(); expect(screen.getByText('Metric name')).toBeInTheDocument();
}); });
@@ -177,7 +192,7 @@ describe('PanelQueryEditor should render right editor', () => {
test.each(cases)('$name', async ({ query, toggle }) => { test.each(cases)('$name', async ({ query, toggle }) => {
config.featureToggles.cloudWatchCrossAccountQuerying = toggle; config.featureToggles.cloudWatchCrossAccountQuerying = toggle;
await act(async () => { await act(async () => {
render(<PanelQueryEditor {...props} datasource={datasourceMock.datasource} query={query} />); render(<QueryEditor {...props} datasource={datasourceMock.datasource} query={query} />);
}); });
expect(await screen.getByText('Monitoring account')).toBeInTheDocument(); expect(await screen.getByText('Monitoring account')).toBeInTheDocument();
}); });
@@ -193,7 +208,7 @@ describe('PanelQueryEditor should render right editor', () => {
{ {
name: 'it is metric query code query and toggle is not enabled', name: 'it is metric query code query and toggle is not enabled',
query: validMetricQueryCodeQuery, query: validMetricQueryCodeQuery,
toggle: true, toggle: false,
}, },
{ name: 'it is logs query and feature is not enabled', query: validLogsQuery, toggle: false }, { name: 'it is logs query and feature is not enabled', query: validLogsQuery, toggle: false },
{ {
@@ -210,10 +225,128 @@ describe('PanelQueryEditor should render right editor', () => {
test.each(cases)('$name', async ({ query, toggle }) => { test.each(cases)('$name', async ({ query, toggle }) => {
config.featureToggles.cloudWatchCrossAccountQuerying = toggle; config.featureToggles.cloudWatchCrossAccountQuerying = toggle;
await act(async () => { await act(async () => {
render(<PanelQueryEditor {...props} datasource={datasourceMock.datasource} query={query} />); render(<QueryEditor {...props} datasource={datasourceMock.datasource} query={query} />);
}); });
expect(await screen.queryByText('Monitoring account')).toBeNull(); expect(await screen.queryByText('Monitoring account')).toBeNull();
}); });
}); });
}); });
describe('QueryHeader', () => {
it('should display metric actions in header when metric query is used', async () => {
await act(async () => {
render(<QueryEditor {...props} query={validMetricQueryCodeQuery} />);
});
expect(screen.getByLabelText(/Region.*/)).toBeInTheDocument();
expect(screen.getByText('CloudWatch Metrics')).toBeInTheDocument();
expect(screen.getByLabelText('Builder')).toBeInTheDocument();
expect(screen.getByLabelText('Code')).toBeInTheDocument();
expect(screen.getByText('Metric Query')).toBeInTheDocument();
});
it('should display metric actions in header when metric query is used', async () => {
await act(async () => {
render(<QueryEditor {...props} query={validLogsQuery} />);
});
expect(screen.getByLabelText(/Region.*/)).toBeInTheDocument();
expect(screen.getByText('CloudWatch Logs')).toBeInTheDocument();
expect(screen.queryByLabelText('Builder')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Code')).not.toBeInTheDocument();
expect(screen.queryByText('Metric Query')).not.toBeInTheDocument();
});
});
describe('metrics editor should handle editor modes correctly', () => {
it('when metric query type is metric search and editor mode is builder', async () => {
await act(async () => {
render(<QueryEditor {...props} query={validMetricSearchBuilderQuery} />);
});
expect(screen.getByText('Metric Search')).toBeInTheDocument();
const radio = screen.getByLabelText('Builder');
expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy();
});
it('when metric query type is metric search and editor mode is raw', async () => {
await act(async () => {
render(<QueryEditor {...props} query={validMetricSearchCodeQuery} />);
});
expect(screen.getByText('Metric Search')).toBeInTheDocument();
const radio = screen.getByLabelText('Code');
expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy();
});
it('when metric query type is metric query and editor mode is builder', async () => {
await act(async () => {
await act(async () => {
render(<QueryEditor {...props} query={validMetricQueryBuilderQuery} />);
});
expect(screen.getByText('Metric Query')).toBeInTheDocument();
const radio = screen.getByLabelText('Builder');
expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy();
});
});
it('when metric query type is metric query and editor mode is raw', async () => {
await act(async () => {
render(<QueryEditor {...props} query={validMetricQueryCodeQuery} />);
});
expect(screen.getByText('Metric Query')).toBeInTheDocument();
const radio = screen.getByLabelText('Code');
expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy();
});
});
describe('confirm modal', () => {
it('should be shown when moving from code editor to builder when in sql mode', async () => {
const sqlQuery = 'SELECT * FROM test';
render(
<QueryEditor
{...props}
query={{ ...validMetricQueryCodeQuery, sqlExpression: sqlQuery }}
onChange={jest.fn()}
onRunQuery={jest.fn()}
/>
);
// the modal should not be shown unless the code editor is "dirty", so need to trigger a change
const codeEditorElement = screen.getByLabelText(FAKE_EDITOR_LABEL);
fireEvent.change(codeEditorElement, { target: { value: 'select * from ' } });
const builderElement = screen.getByLabelText('Builder');
expect(builderElement).toBeInTheDocument();
await act(async () => {
await builderElement.click();
});
const modalTitleElem = screen.getByText('Are you sure?');
expect(modalTitleElem).toBeInTheDocument();
});
it('should not be shown when moving from builder to code when in sql mode', async () => {
render(
<QueryEditor {...props} query={validMetricQueryBuilderQuery} onChange={jest.fn()} onRunQuery={jest.fn()} />
);
const builderElement = screen.getByLabelText('Builder');
expect(builderElement).toBeInTheDocument();
await act(async () => {
await builderElement.click();
});
expect(screen.queryByText('Are you sure?')).toBeNull();
});
it('should not be shown when moving from code to builder when in search mode', async () => {
render(<QueryEditor {...props} query={validMetricSearchCodeQuery} onChange={jest.fn()} onRunQuery={jest.fn()} />);
const builderElement = screen.getByLabelText('Builder');
expect(builderElement).toBeInTheDocument();
await act(async () => {
await builderElement.click();
});
expect(screen.queryByText('Are you sure?')).toBeNull();
});
});
}); });

View File

@@ -0,0 +1,55 @@
import React, { useCallback, useEffect, useState } from 'react';
import { QueryEditorProps } from '@grafana/data';
import { CloudWatchDatasource } from '../datasource';
import { isCloudWatchLogsQuery, isCloudWatchMetricsQuery } from '../guards';
import { CloudWatchJsonData, CloudWatchQuery } from '../types';
import LogsQueryEditor from './LogsQueryEditor';
import { MetricsQueryEditor } from './MetricsQueryEditor/MetricsQueryEditor';
import QueryHeader from './QueryHeader';
export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>;
export const QueryEditor = (props: Props) => {
const { query, onChange, data } = props;
const [dataIsStale, setDataIsStale] = useState(false);
const [extraHeaderElementLeft, setExtraHeaderElementLeft] = useState<JSX.Element>();
const [extraHeaderElementRight, setExtraHeaderElementRight] = useState<JSX.Element>();
useEffect(() => {
setDataIsStale(false);
}, [data]);
const onChangeInternal = useCallback(
(query: CloudWatchQuery) => {
setDataIsStale(true);
onChange(query);
},
[onChange]
);
return (
<>
<QueryHeader
{...props}
extraHeaderElementLeft={extraHeaderElementLeft}
extraHeaderElementRight={extraHeaderElementRight}
dataIsStale={dataIsStale}
/>
{isCloudWatchMetricsQuery(query) && (
<MetricsQueryEditor
{...props}
query={query}
onRunQuery={() => {}}
onChange={onChangeInternal}
extraHeaderElementLeft={setExtraHeaderElementLeft}
extraHeaderElementRight={setExtraHeaderElementRight}
/>
)}
{isCloudWatchLogsQuery(query) && <LogsQueryEditor {...props} query={query} onChange={onChangeInternal} />}
</>
);
};

View File

@@ -6,7 +6,6 @@ import { config } from '@grafana/runtime';
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource'; import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
import { validLogsQuery, validMetricSearchBuilderQuery } from '../__mocks__/queries'; import { validLogsQuery, validMetricSearchBuilderQuery } from '../__mocks__/queries';
import { CloudWatchLogsQuery, CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types';
import QueryHeader from './QueryHeader'; import QueryHeader from './QueryHeader';
@@ -20,74 +19,6 @@ describe('QueryHeader', () => {
afterEach(() => { afterEach(() => {
config.featureToggles.cloudWatchCrossAccountQuerying = originalFeatureToggleValue; config.featureToggles.cloudWatchCrossAccountQuerying = originalFeatureToggleValue;
}); });
it('should display metric options for metrics', async () => {
const query: CloudWatchMetricsQuery = {
queryMode: 'Metrics',
id: '',
region: 'us-east-2',
namespace: '',
period: '',
alias: '',
metricName: '',
dimensions: {},
matchExact: true,
statistic: '',
expression: '',
refId: '',
};
const onChange = jest.fn();
const onRunQuery = jest.fn();
query.metricEditorMode = MetricEditorMode.Code;
query.metricQueryType = MetricQueryType.Query;
render(
<QueryHeader
sqlCodeEditorIsDirty={true}
datasource={ds.datasource}
query={query}
onChange={onChange}
onRunQuery={onRunQuery}
/>
);
const builderElement = screen.getByLabelText('Builder');
expect(builderElement).toBeInTheDocument();
await act(async () => {
await builderElement.click();
});
const modalTitleElem = screen.getByText('Are you sure?');
expect(modalTitleElem).toBeInTheDocument();
expect(onChange).not.toHaveBeenCalled();
});
it('should not display metric options for logs', async () => {
const onChange = jest.fn();
const onRunQuery = jest.fn();
const query: CloudWatchLogsQuery = {
queryType: 'Metrics',
id: '',
region: 'us-east-2',
expression: '',
refId: '',
queryMode: 'Logs',
};
render(
<QueryHeader
sqlCodeEditorIsDirty={true}
datasource={ds.datasource}
query={query}
onChange={onChange}
onRunQuery={onRunQuery}
/>
);
await waitFor(() => {
expect(screen.queryByLabelText('Builder')).toBeNull();
expect(screen.queryByLabelText('Code')).toBeNull();
});
});
describe('when changing region', () => { describe('when changing region', () => {
const { datasource } = setupMockedDataSource(); const { datasource } = setupMockedDataSource();
@@ -101,11 +32,11 @@ describe('QueryHeader', () => {
datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false); datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false);
render( render(
<QueryHeader <QueryHeader
sqlCodeEditorIsDirty={true}
datasource={datasource} datasource={datasource}
query={{ ...validMetricSearchBuilderQuery, region: 'us-east-1', accountId: 'all' }} query={{ ...validMetricSearchBuilderQuery, region: 'us-east-1', accountId: 'all' }}
onChange={onChange} onChange={onChange}
onRunQuery={jest.fn()} onRunQuery={jest.fn()}
dataIsStale={false}
/> />
); );
await waitFor(() => expect(screen.queryByText('us-east-1')).toBeInTheDocument()); await waitFor(() => expect(screen.queryByText('us-east-1')).toBeInTheDocument());
@@ -126,11 +57,11 @@ describe('QueryHeader', () => {
render( render(
<QueryHeader <QueryHeader
sqlCodeEditorIsDirty={true}
datasource={datasource} datasource={datasource}
query={{ ...validMetricSearchBuilderQuery, region: 'us-east-1', accountId: '123' }} query={{ ...validMetricSearchBuilderQuery, region: 'us-east-1', accountId: '123' }}
onChange={onChange} onChange={onChange}
onRunQuery={jest.fn()} onRunQuery={jest.fn()}
dataIsStale={false}
/> />
); );
await waitFor(() => expect(screen.queryByText('us-east-1')).toBeInTheDocument()); await waitFor(() => expect(screen.queryByText('us-east-1')).toBeInTheDocument());
@@ -151,7 +82,7 @@ describe('QueryHeader', () => {
render( render(
<QueryHeader <QueryHeader
sqlCodeEditorIsDirty={true} dataIsStale={false}
datasource={datasource} datasource={datasource}
query={{ ...validLogsQuery, region: 'us-east-1' }} query={{ ...validLogsQuery, region: 'us-east-1' }}
onChange={onChange} onChange={onChange}
@@ -172,7 +103,7 @@ describe('QueryHeader', () => {
render( render(
<QueryHeader <QueryHeader
sqlCodeEditorIsDirty={true} dataIsStale={false}
datasource={datasource} datasource={datasource}
query={{ ...validLogsQuery, region: 'us-east-1' }} query={{ ...validLogsQuery, region: 'us-east-1' }}
onChange={onChange} onChange={onChange}

View File

@@ -1,23 +1,19 @@
import React from 'react'; import React from 'react';
import { SelectableValue, ExploreMode } from '@grafana/data'; import { CoreApp, LoadingState, QueryEditorProps, SelectableValue } from '@grafana/data';
import { EditorHeader, InlineSelect, FlexItem } from '@grafana/experimental'; import { EditorHeader, InlineSelect, FlexItem } from '@grafana/experimental';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { Badge } from '@grafana/ui'; import { Badge, Button } from '@grafana/ui';
import { CloudWatchDatasource } from '../datasource'; import { CloudWatchDatasource } from '../datasource';
import { isCloudWatchMetricsQuery } from '../guards'; import { isCloudWatchMetricsQuery } from '../guards';
import { useIsMonitoringAccount, useRegions } from '../hooks'; import { useIsMonitoringAccount, useRegions } from '../hooks';
import { CloudWatchQuery, CloudWatchQueryMode } from '../types'; import { CloudWatchJsonData, CloudWatchQuery, CloudWatchQueryMode, MetricQueryType } from '../types';
import MetricsQueryHeader from './MetricsQueryEditor/MetricsQueryHeader'; export interface Props extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> {
extraHeaderElementLeft?: JSX.Element;
interface QueryHeaderProps { extraHeaderElementRight?: JSX.Element;
query: CloudWatchQuery; dataIsStale: boolean;
datasource: CloudWatchDatasource;
onChange: (query: CloudWatchQuery) => void;
onRunQuery: () => void;
sqlCodeEditorIsDirty: boolean;
} }
const apiModes: Array<SelectableValue<CloudWatchQueryMode>> = [ const apiModes: Array<SelectableValue<CloudWatchQueryMode>> = [
@@ -25,18 +21,27 @@ const apiModes: Array<SelectableValue<CloudWatchQueryMode>> = [
{ label: 'CloudWatch Logs', value: 'Logs' }, { label: 'CloudWatch Logs', value: 'Logs' },
]; ];
const QueryHeader: React.FC<QueryHeaderProps> = ({ query, sqlCodeEditorIsDirty, datasource, onChange, onRunQuery }) => { const QueryHeader: React.FC<Props> = ({
query,
onChange,
datasource,
extraHeaderElementLeft,
extraHeaderElementRight,
dataIsStale,
data,
onRunQuery,
}) => {
const { queryMode, region } = query; const { queryMode, region } = query;
const isMonitoringAccount = useIsMonitoringAccount(datasource.api, query.region); const isMonitoringAccount = useIsMonitoringAccount(datasource.api, query.region);
const [regions, regionIsLoading] = useRegions(datasource); const [regions, regionIsLoading] = useRegions(datasource);
const onQueryModeChange = ({ value }: SelectableValue<CloudWatchQueryMode>) => { const onQueryModeChange = ({ value }: SelectableValue<CloudWatchQueryMode>) => {
if (value !== queryMode) { if (value && value !== queryMode) {
onChange({ onChange({
...datasource.getDefaultQuery(CoreApp.Unknown),
...query, ...query,
queryMode: value, queryMode: value,
} as CloudWatchQuery); });
} }
}; };
const onRegionChange = async (region: string) => { const onRegionChange = async (region: string) => {
@@ -49,44 +54,60 @@ const QueryHeader: React.FC<QueryHeaderProps> = ({ query, sqlCodeEditorIsDirty,
}; };
const shouldDisplayMonitoringBadge = const shouldDisplayMonitoringBadge =
queryMode === 'Logs' && isMonitoringAccount && config.featureToggles.cloudWatchCrossAccountQuerying; config.featureToggles.cloudWatchCrossAccountQuerying &&
isMonitoringAccount &&
(query.queryMode === 'Logs' ||
(isCloudWatchMetricsQuery(query) && query.metricQueryType === MetricQueryType.Search));
return ( return (
<EditorHeader> <>
<InlineSelect <EditorHeader>
label="Region" <InlineSelect
value={region} label="Region"
placeholder="Select region" value={region}
allowCustomValue placeholder="Select region"
onChange={({ value: region }) => region && onRegionChange(region)} allowCustomValue
options={regions} onChange={({ value: region }) => region && onRegionChange(region)}
isLoading={regionIsLoading} options={regions}
/> isLoading={regionIsLoading}
<InlineSelect aria-label="Query mode" value={queryMode} options={apiModes} onChange={onQueryModeChange} />
{shouldDisplayMonitoringBadge && (
<>
<FlexItem grow={1} />
<Badge
text="Monitoring account"
color="blue"
tooltip="AWS monitoring accounts view data from source accounts so you can centralize monitoring and troubleshoot activites"
></Badge>
</>
)}
{queryMode === ExploreMode.Metrics && (
<MetricsQueryHeader
query={query}
datasource={datasource}
onChange={onChange}
onRunQuery={onRunQuery}
isMonitoringAccount={isMonitoringAccount}
sqlCodeEditorIsDirty={sqlCodeEditorIsDirty}
/> />
)}
</EditorHeader> <InlineSelect
aria-label="Query mode"
value={queryMode}
options={apiModes}
onChange={onQueryModeChange}
inputId={`cloudwatch-query-mode-${query.refId}`}
id={`cloudwatch-query-mode-${query.refId}`}
/>
{extraHeaderElementLeft}
<FlexItem grow={1} />
{shouldDisplayMonitoringBadge && (
<>
<Badge
text="Monitoring account"
color="blue"
tooltip="AWS monitoring accounts view data from source accounts so you can centralize monitoring and troubleshoot activites"
></Badge>
</>
)}
<Button
variant={dataIsStale ? 'primary' : 'secondary'}
size="sm"
onClick={onRunQuery}
icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined}
disabled={data?.state === LoadingState.Loading}
>
Run queries
</Button>
{extraHeaderElementRight}
</EditorHeader>
</>
); );
}; };

View File

@@ -32,7 +32,6 @@ describe('Cloudwatch SQLBuilderEditor', () => {
query: makeSQLQuery(), query: makeSQLQuery(),
datasource, datasource,
onChange: () => {}, onChange: () => {},
onRunQuery: () => {},
}; };
it('Displays the namespace', async () => { it('Displays the namespace', async () => {

View File

@@ -17,10 +17,9 @@ export type Props = {
query: CloudWatchMetricsQuery; query: CloudWatchMetricsQuery;
datasource: CloudWatchDatasource; datasource: CloudWatchDatasource;
onChange: (value: CloudWatchMetricsQuery) => void; onChange: (value: CloudWatchMetricsQuery) => void;
onRunQuery: () => void;
}; };
export function SQLBuilderEditor({ query, datasource, onChange, onRunQuery }: React.PropsWithChildren<Props>) { export function SQLBuilderEditor({ query, datasource, onChange }: React.PropsWithChildren<Props>) {
const sql = query.sql ?? {}; const sql = query.sql ?? {};
const onQueryChange = useCallback( const onQueryChange = useCallback(
@@ -33,9 +32,8 @@ export function SQLBuilderEditor({ query, datasource, onChange, onRunQuery }: Re
}; };
onChange(fullQuery); onChange(fullQuery);
onRunQuery();
}, },
[onChange, onRunQuery] [onChange]
); );
const [sqlPreview, setSQLPreview] = useState<string | undefined>(); const [sqlPreview, setSQLPreview] = useState<string | undefined>();

View File

@@ -12,11 +12,10 @@ export interface Props {
region: string; region: string;
sql: string; sql: string;
onChange: (sql: string) => void; onChange: (sql: string) => void;
onRunQuery: () => void;
datasource: CloudWatchDatasource; datasource: CloudWatchDatasource;
} }
export const SQLCodeEditor: FunctionComponent<Props> = ({ region, sql, onChange, onRunQuery, datasource }) => { export const SQLCodeEditor: FunctionComponent<Props> = ({ region, sql, onChange, datasource }) => {
useEffect(() => { useEffect(() => {
datasource.sqlCompletionItemProvider.setRegion(region); datasource.sqlCompletionItemProvider.setRegion(region);
}, [region, datasource]); }, [region, datasource]);
@@ -27,10 +26,9 @@ export const SQLCodeEditor: FunctionComponent<Props> = ({ region, sql, onChange,
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {
const text = editor.getValue(); const text = editor.getValue();
onChange(text); onChange(text);
onRunQuery();
}); });
}, },
[onChange, onRunQuery] [onChange]
); );
return ( return (

View File

@@ -1,8 +1,7 @@
export { Dimensions } from './Dimensions/Dimensions'; export { Dimensions } from './Dimensions/Dimensions';
export { QueryInlineField, QueryField } from './Forms'; export { QueryInlineField, QueryField } from './Forms';
export { PanelQueryEditor } from './PanelQueryEditor'; export { QueryEditor as PanelQueryEditor } from './QueryEditor';
export { CloudWatchLogsQueryEditor } from './LogsQueryEditor'; export { CloudWatchLogsQueryEditor } from './LogsQueryEditor';
export { MetricStatEditor } from './MetricStatEditor'; export { MetricStatEditor } from './MetricStatEditor';
export { SQLBuilderEditor } from './SQLBuilderEditor'; export { SQLBuilderEditor } from './SQLBuilderEditor';
export { MathExpressionQueryField } from './MathExpressionQueryField'; export { MathExpressionQueryField } from './MathExpressionQueryField';
export { SQLCodeEditor } from './SQLCodeEditor';

View File

@@ -159,7 +159,7 @@ export class CloudWatchDatasource
} }
getQueryDisplayText(query: CloudWatchQuery) { getQueryDisplayText(query: CloudWatchQuery) {
if (query.queryMode === 'Logs') { if (isCloudWatchLogsQuery(query)) {
return query.expression ?? ''; return query.expression ?? '';
} else { } else {
return JSON.stringify(query); return JSON.stringify(query);

View File

@@ -4,7 +4,7 @@ import { getAppEvents } from '@grafana/runtime';
import { ConfigEditor } from './components/ConfigEditor'; import { ConfigEditor } from './components/ConfigEditor';
import LogsCheatSheet from './components/LogsCheatSheet'; import LogsCheatSheet from './components/LogsCheatSheet';
import { MetaInspector } from './components/MetaInspector'; import { MetaInspector } from './components/MetaInspector';
import { PanelQueryEditor } from './components/PanelQueryEditor'; import { QueryEditor } from './components/QueryEditor';
import { CloudWatchDatasource } from './datasource'; import { CloudWatchDatasource } from './datasource';
import { onDashboardLoadedHandler } from './tracking'; import { onDashboardLoadedHandler } from './tracking';
import { CloudWatchJsonData, CloudWatchQuery } from './types'; import { CloudWatchJsonData, CloudWatchQuery } from './types';
@@ -14,7 +14,7 @@ export const plugin = new DataSourcePlugin<CloudWatchDatasource, CloudWatchQuery
) )
.setQueryEditorHelp(LogsCheatSheet) .setQueryEditorHelp(LogsCheatSheet)
.setConfigEditor(ConfigEditor) .setConfigEditor(ConfigEditor)
.setQueryEditor(PanelQueryEditor) .setQueryEditor(QueryEditor)
.setMetadataInspector(MetaInspector); .setMetadataInspector(MetaInspector);
// Subscribe to on dashboard loaded event so that we can track plugin adoption // Subscribe to on dashboard loaded event so that we can track plugin adoption

View File

@@ -41,7 +41,7 @@ export interface SQLExpression {
} }
export interface CloudWatchMetricsQuery extends MetricStat, DataQuery { export interface CloudWatchMetricsQuery extends MetricStat, DataQuery {
queryMode?: 'Metrics'; queryMode?: CloudWatchQueryMode;
metricQueryType?: MetricQueryType; metricQueryType?: MetricQueryType;
metricEditorMode?: MetricEditorMode; metricEditorMode?: MetricEditorMode;
@@ -96,7 +96,7 @@ export enum CloudWatchLogsQueryStatus {
} }
export interface CloudWatchLogsQuery extends DataQuery { export interface CloudWatchLogsQuery extends DataQuery {
queryMode: 'Logs'; queryMode: CloudWatchQueryMode;
id: string; id: string;
region: string; region: string;
expression?: string; expression?: string;
@@ -115,7 +115,7 @@ export type CloudWatchQuery =
| CloudWatchDefaultQuery; | CloudWatchDefaultQuery;
export interface CloudWatchAnnotationQuery extends MetricStat, DataQuery { export interface CloudWatchAnnotationQuery extends MetricStat, DataQuery {
queryMode: 'Annotations'; queryMode: CloudWatchQueryMode;
prefixMatching?: boolean; prefixMatching?: boolean;
actionPrefix?: string; actionPrefix?: string;
alarmNamePrefix?: string; alarmNamePrefix?: string;