mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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"]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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;
|
|
||||||
@@ -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} />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -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} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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';
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user