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:
parent
66c076f24e
commit
6928ad2949
@ -5346,8 +5346,7 @@ exports[`better eslint`] = {
|
||||
"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.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx:5381": [
|
||||
[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.", "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.", "1"],
|
||||
[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": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
@ -38,32 +38,30 @@ export const validMetricSearchBuilderQuery: CloudWatchMetricsQuery = {
|
||||
};
|
||||
|
||||
export const validMetricQueryBuilderQuery: CloudWatchMetricsQuery = {
|
||||
id: '',
|
||||
queryMode: 'Metrics',
|
||||
region: 'us-east-2',
|
||||
namespace: 'AWS/EC2',
|
||||
period: '3000',
|
||||
alias: '',
|
||||
metricName: 'CPUUtilization',
|
||||
dimensions: { InstanceId: 'i-123' },
|
||||
matchExact: true,
|
||||
statistic: 'Average',
|
||||
refId: '',
|
||||
id: '',
|
||||
region: 'us-east-1',
|
||||
namespace: 'ec2',
|
||||
dimensions: { somekey: 'somevalue' },
|
||||
metricQueryType: MetricQueryType.Query,
|
||||
metricEditorMode: MetricEditorMode.Builder,
|
||||
sql: {
|
||||
select: {
|
||||
from: {
|
||||
type: QueryEditorExpressionType.Function,
|
||||
name: 'AVERAGE',
|
||||
name: 'SCHEMA',
|
||||
parameters: [
|
||||
{
|
||||
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 = {
|
||||
|
@ -45,7 +45,6 @@ export const AnnotationQueryEditor = (props: Props) => {
|
||||
metricStat={query}
|
||||
disableExpressions={true}
|
||||
onChange={(metricStat: MetricStat) => onChange({ ...query, ...metricStat })}
|
||||
onRunQuery={() => {}}
|
||||
></MetricStatEditor>
|
||||
<Space v={0.5} />
|
||||
<EditorRow>
|
||||
|
@ -35,7 +35,6 @@ const defaultProps = {
|
||||
},
|
||||
]),
|
||||
onChange: jest.fn(),
|
||||
onRunQuery: jest.fn(),
|
||||
};
|
||||
|
||||
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>>;
|
||||
fetchLogGroups: (params: Partial<DescribeLogGroupsRequest>) => Promise<SelectableResourceValue[]>;
|
||||
onChange: (selectedLogGroups: SelectableResourceValue[]) => void;
|
||||
onRunQuery: () => void;
|
||||
};
|
||||
|
||||
export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) => {
|
||||
@ -30,7 +29,6 @@ export const CrossAccountLogsQueryField = (props: CrossAccountLogsQueryProps) =>
|
||||
const toggleModal = () => {
|
||||
setIsModalOpen(!isModalOpen);
|
||||
if (isModalOpen) {
|
||||
props.onRunQuery();
|
||||
} else {
|
||||
setSelectedLogGroups(props.selectedLogGroups);
|
||||
searchFn(searchPhrase, searchAccountId);
|
||||
|
@ -33,7 +33,6 @@ const props = {
|
||||
query: q,
|
||||
disableExpressions: false,
|
||||
onChange: jest.fn(),
|
||||
onRunQuery: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Dimensions', () => {
|
||||
|
@ -13,12 +13,11 @@ const dynamicLabelsCompletionItemProvider = new DynamicLabelsCompletionItemProvi
|
||||
|
||||
export interface Props {
|
||||
onChange: (query: string) => void;
|
||||
onRunQuery: () => void;
|
||||
label: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export function DynamicLabelsField({ label, width, onChange, onRunQuery }: Props) {
|
||||
export function DynamicLabelsField({ label, width, onChange }: Props) {
|
||||
const theme = useTheme2();
|
||||
const styles = getInputStyles({ theme, width });
|
||||
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, () => {
|
||||
const text = editor.getValue();
|
||||
onChange(text);
|
||||
onRunQuery();
|
||||
});
|
||||
|
||||
const containerDiv = containerRef.current;
|
||||
containerDiv !== null && editor.layout({ width: containerDiv.clientWidth, height: containerDiv.clientHeight });
|
||||
},
|
||||
[onChange, onRunQuery]
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -69,7 +67,6 @@ export function DynamicLabelsField({ label, width, onChange, onRunQuery }: Props
|
||||
onBlur={(value) => {
|
||||
if (value !== label) {
|
||||
onChange(value);
|
||||
onRunQuery();
|
||||
}
|
||||
}}
|
||||
onBeforeEditorMount={(monaco: Monaco) =>
|
||||
|
@ -22,7 +22,6 @@ const defaultProps = {
|
||||
refId: '',
|
||||
} as CloudWatchLogsQuery,
|
||||
onChange: jest.fn(),
|
||||
onRunQuery: jest.fn(),
|
||||
};
|
||||
describe('LogGroupSelection', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -16,14 +16,13 @@ type Props = {
|
||||
datasource: CloudWatchDatasource;
|
||||
query: CloudWatchLogsQuery;
|
||||
onChange: (value: CloudWatchQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
};
|
||||
|
||||
const rowGap = css`
|
||||
gap: 3px;
|
||||
`;
|
||||
|
||||
export const LogGroupSelection = ({ datasource, query, onChange, onRunQuery }: Props) => {
|
||||
export const LogGroupSelection = ({ datasource, query, onChange }: Props) => {
|
||||
const accountState = useAccountOptions(datasource.api, query.region);
|
||||
|
||||
return (
|
||||
@ -37,7 +36,6 @@ export const LogGroupSelection = ({ datasource, query, onChange, onRunQuery }: P
|
||||
onChange({ ...query, logGroups: selectedLogGroups, logGroupNames: [] });
|
||||
}}
|
||||
accountOptions={accountState.value}
|
||||
onRunQuery={onRunQuery}
|
||||
selectedLogGroups={query.logGroups ?? []} /* todo handle defaults */
|
||||
/>
|
||||
) : (
|
||||
@ -53,7 +51,6 @@ export const LogGroupSelection = ({ datasource, query, onChange, onRunQuery }: P
|
||||
onChange={function (logGroupNames: string[]): void {
|
||||
onChange({ ...query, logGroupNames, logGroups: [] });
|
||||
}}
|
||||
onRunQuery={onRunQuery}
|
||||
refId={query.refId}
|
||||
/>
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ export interface LogGroupSelectorProps {
|
||||
onChange: (logGroups: string[]) => void;
|
||||
|
||||
datasource?: CloudWatchDatasource;
|
||||
onRunQuery?: () => void;
|
||||
onOpenMenu?: () => Promise<void>;
|
||||
refId?: string;
|
||||
width?: number | 'auto';
|
||||
@ -33,7 +32,6 @@ export const LogGroupSelector: React.FC<LogGroupSelectorProps> = ({
|
||||
selectedLogGroups,
|
||||
onChange,
|
||||
datasource,
|
||||
onRunQuery,
|
||||
onOpenMenu,
|
||||
refId,
|
||||
width,
|
||||
@ -135,7 +133,6 @@ export const LogGroupSelector: React.FC<LogGroupSelectorProps> = ({
|
||||
options={datasource ? appendTemplateVariables(datasource, logGroupOptions) : logGroupOptions}
|
||||
value={selectedLogGroups}
|
||||
onChange={(v) => onChange(v.filter(({ value }) => value).map(({ value }) => value))}
|
||||
onBlur={onRunQuery}
|
||||
closeMenuOnSelect={false}
|
||||
isClearable
|
||||
isOptionDisabled={() => selectedLogGroups.length >= MAX_LOG_GROUPS}
|
||||
|
@ -2,7 +2,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
// Types
|
||||
import { AbsoluteTimeRange, QueryEditorProps } from '@grafana/data';
|
||||
import { InlineFormLabel } from '@grafana/ui';
|
||||
|
||||
@ -22,7 +21,7 @@ const labelClass = css`
|
||||
`;
|
||||
|
||||
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;
|
||||
if (data?.request?.range?.from) {
|
||||
@ -40,13 +39,9 @@ export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor
|
||||
|
||||
return (
|
||||
<CloudWatchLogsQueryField
|
||||
{...props}
|
||||
exploreId={exploreId}
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
history={[]}
|
||||
data={data}
|
||||
absoluteRange={absolute}
|
||||
ExtraFieldElement={
|
||||
<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 React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { ExploreId } from '../../../../types';
|
||||
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
|
||||
@ -13,28 +12,6 @@ jest
|
||||
.mockImplementation((func: (...args: any) => any, wait?: number) => func as DebouncedFunc<typeof func>);
|
||||
|
||||
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 () => {
|
||||
const onRunQuery = jest.fn();
|
||||
const ds = setupMockedDataSource();
|
||||
|
@ -24,7 +24,6 @@ import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../typ
|
||||
import { getStatsGroups } from '../utils/query/getStatsGroups';
|
||||
|
||||
import { LogGroupSelection } from './LogGroupSelection';
|
||||
import QueryHeader from './QueryHeader';
|
||||
|
||||
export interface CloudWatchLogsQueryFieldProps
|
||||
extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>,
|
||||
@ -46,7 +45,7 @@ const plugins: Array<Plugin<Editor>> = [
|
||||
),
|
||||
];
|
||||
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 cleanText = datasource.languageProvider.cleanText;
|
||||
@ -85,21 +84,13 @@ export const CloudWatchLogsQueryField = (props: CloudWatchLogsQueryFieldProps) =
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryHeader
|
||||
query={query}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
sqlCodeEditorIsDirty={false}
|
||||
/>
|
||||
<LogGroupSelection datasource={datasource} query={query} onChange={onChange} onRunQuery={onRunQuery} />
|
||||
<LogGroupSelection datasource={datasource} query={query} onChange={onChange} />
|
||||
<div className="gf-form-inline gf-form-inline--nowrap flex-grow-1">
|
||||
<div className="gf-form gf-form--grow flex-shrink-1">
|
||||
<QueryField
|
||||
additionalPlugins={plugins}
|
||||
query={query.expression ?? ''}
|
||||
onChange={onChangeQuery}
|
||||
onRunQuery={props.onRunQuery}
|
||||
onTypeahead={onTypeahead}
|
||||
cleanText={cleanText}
|
||||
placeholder="Enter a CloudWatch Logs Insights query (run with Shift+Enter)"
|
||||
|
@ -10,17 +10,11 @@ import { registerLanguage } from '../monarch/register';
|
||||
|
||||
export interface Props {
|
||||
onChange: (query: string) => void;
|
||||
onRunQuery: () => void;
|
||||
expression: string;
|
||||
datasource: CloudWatchDatasource;
|
||||
}
|
||||
|
||||
export function MathExpressionQueryField({
|
||||
expression: query,
|
||||
onChange,
|
||||
onRunQuery,
|
||||
datasource,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
export function MathExpressionQueryField({ expression: query, onChange, datasource }: React.PropsWithChildren<Props>) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const onEditorMount = useCallback(
|
||||
(editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
||||
@ -28,7 +22,6 @@ export function MathExpressionQueryField({
|
||||
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {
|
||||
const text = editor.getValue();
|
||||
onChange(text);
|
||||
onRunQuery();
|
||||
});
|
||||
|
||||
// auto resizes the editor to be the height of the content it holds
|
||||
@ -48,7 +41,7 @@ export function MathExpressionQueryField({
|
||||
editor.onDidContentSizeChange(updateElementHeight);
|
||||
updateElementHeight();
|
||||
},
|
||||
[onChange, onRunQuery]
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -77,7 +70,6 @@ export function MathExpressionQueryField({
|
||||
onBlur={(value) => {
|
||||
if (value !== query) {
|
||||
onChange(value);
|
||||
onRunQuery();
|
||||
}
|
||||
}}
|
||||
onBeforeEditorMount={(monaco: Monaco) =>
|
||||
|
@ -33,7 +33,6 @@ const props = {
|
||||
datasource: ds.datasource,
|
||||
metricStat,
|
||||
onChange: jest.fn(),
|
||||
onRunQuery: jest.fn(),
|
||||
};
|
||||
|
||||
describe('MetricStatEditor', () => {
|
||||
@ -43,10 +42,9 @@ describe('MetricStatEditor', () => {
|
||||
describe('statistics field', () => {
|
||||
test.each([['Average', 'p23.23', 'p34', '$statistic']])('should accept valid values', async (statistic) => {
|
||||
const onChange = jest.fn();
|
||||
const onRunQuery = jest.fn();
|
||||
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');
|
||||
expect(statisticElement).toBeInTheDocument();
|
||||
@ -54,14 +52,12 @@ describe('MetricStatEditor', () => {
|
||||
await userEvent.type(statisticElement, statistic);
|
||||
fireEvent.keyDown(statisticElement, { keyCode: 13 });
|
||||
expect(onChange).toHaveBeenCalledWith({ ...props.metricStat, statistic });
|
||||
expect(onRunQuery).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each([['CustomStat', 'p23,23', '$statistic']])('should not accept invalid values', async (statistic) => {
|
||||
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');
|
||||
expect(statisticElement).toBeInTheDocument();
|
||||
@ -69,7 +65,6 @@ describe('MetricStatEditor', () => {
|
||||
await userEvent.type(statisticElement, statistic);
|
||||
fireEvent.keyDown(statisticElement, { keyCode: 13 });
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
expect(onRunQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -120,18 +115,15 @@ describe('MetricStatEditor', () => {
|
||||
{ value: 'm2', label: 'm2', text: 'm2' },
|
||||
];
|
||||
const onChange = jest.fn();
|
||||
const onRunQuery = jest.fn();
|
||||
const propsNamespaceMetrics = {
|
||||
...props,
|
||||
onChange,
|
||||
onRunQuery,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
propsNamespaceMetrics.datasource.api.getNamespaces = jest.fn().mockResolvedValue(namespaces);
|
||||
propsNamespaceMetrics.datasource.api.getMetrics = jest.fn().mockResolvedValue(metrics);
|
||||
onChange.mockClear();
|
||||
onRunQuery.mockClear();
|
||||
});
|
||||
|
||||
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, 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 () => {
|
||||
|
@ -19,7 +19,6 @@ export type Props = {
|
||||
datasource: CloudWatchDatasource;
|
||||
disableExpressions?: boolean;
|
||||
onChange: (value: MetricStat) => void;
|
||||
onRunQuery: () => void;
|
||||
};
|
||||
|
||||
export function MetricStatEditor({
|
||||
@ -28,7 +27,6 @@ export function MetricStatEditor({
|
||||
datasource,
|
||||
disableExpressions = false,
|
||||
onChange,
|
||||
onRunQuery,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const namespaces = useNamespaces(datasource);
|
||||
const metrics = useMetrics(datasource, metricStat);
|
||||
@ -47,14 +45,9 @@ export function MetricStatEditor({
|
||||
});
|
||||
}, [accountState, metricStat, onChange, datasource.api]);
|
||||
|
||||
const onMetricStatChange = (metricStat: MetricStat) => {
|
||||
onChange(metricStat);
|
||||
onRunQuery();
|
||||
};
|
||||
|
||||
const onNamespaceChange = async (metricStat: MetricStat) => {
|
||||
const validatedQuery = await validateMetricName(metricStat);
|
||||
onMetricStatChange(validatedQuery);
|
||||
onChange(validatedQuery);
|
||||
};
|
||||
|
||||
const validateMetricName = async (metricStat: MetricStat) => {
|
||||
@ -78,7 +71,6 @@ export function MetricStatEditor({
|
||||
accountId={metricStat.accountId}
|
||||
onChange={(accountId?: string) => {
|
||||
onChange({ ...metricStat, accountId });
|
||||
onRunQuery();
|
||||
}}
|
||||
accountOptions={accountState?.value || []}
|
||||
></Account>
|
||||
@ -105,7 +97,7 @@ export function MetricStatEditor({
|
||||
options={metrics}
|
||||
onChange={({ value: metricName }) => {
|
||||
if (metricName) {
|
||||
onMetricStatChange({ ...metricStat, metricName });
|
||||
onChange({ ...metricStat, metricName });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@ -130,7 +122,7 @@ export function MetricStatEditor({
|
||||
return;
|
||||
}
|
||||
|
||||
onMetricStatChange({ ...metricStat, statistic });
|
||||
onChange({ ...metricStat, statistic });
|
||||
}}
|
||||
/>
|
||||
</EditorField>
|
||||
@ -141,7 +133,7 @@ export function MetricStatEditor({
|
||||
<EditorField label="Dimensions">
|
||||
<Dimensions
|
||||
metricStat={metricStat}
|
||||
onChange={(dimensions) => onMetricStatChange({ ...metricStat, dimensions })}
|
||||
onChange={(dimensions) => onChange({ ...metricStat, dimensions })}
|
||||
dimensionKeys={dimensionKeys}
|
||||
disableExpressions={disableExpressions}
|
||||
datasource={datasource}
|
||||
@ -157,7 +149,7 @@ export function MetricStatEditor({
|
||||
id={`${refId}-cloudwatch-match-exact`}
|
||||
value={!!metricStat.matchExact}
|
||||
onChange={(e) => {
|
||||
onMetricStatChange({
|
||||
onChange({
|
||||
...metricStat,
|
||||
matchExact: e.currentTarget.checked,
|
||||
});
|
||||
|
@ -69,6 +69,8 @@ const setup = () => {
|
||||
metricQueryType: MetricQueryType.Search,
|
||||
metricEditorMode: MetricEditorMode.Builder,
|
||||
},
|
||||
extraHeaderElementLeft: () => {},
|
||||
extraHeaderElementRight: () => {},
|
||||
datasource,
|
||||
history: [],
|
||||
onChange: jest.fn(),
|
||||
@ -79,66 +81,6 @@ const setup = () => {
|
||||
};
|
||||
|
||||
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', () => {
|
||||
it('should display match exact switch', async () => {
|
||||
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 { EditorField, EditorRow, Space } from '@grafana/experimental';
|
||||
import { QueryEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { EditorField, EditorRow, InlineSelect, Space } from '@grafana/experimental';
|
||||
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 { isCloudWatchMetricsQuery } from '../../guards';
|
||||
import useMigratedMetricsQuery from '../../migrations/useMigratedMetricsQuery';
|
||||
import {
|
||||
CloudWatchJsonData,
|
||||
@ -18,39 +17,99 @@ import {
|
||||
MetricStat,
|
||||
} from '../../types';
|
||||
import { DynamicLabelsField } from '../DynamicLabelsField';
|
||||
import QueryHeader from '../QueryHeader';
|
||||
import { SQLCodeEditor } from '../SQLCodeEditor';
|
||||
|
||||
import { Alias } from './Alias';
|
||||
|
||||
export interface Props extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> {
|
||||
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) => {
|
||||
const { query, onRunQuery, datasource } = props;
|
||||
const { query, datasource, extraHeaderElementLeft, extraHeaderElementRight, onChange } = props;
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [sqlCodeEditorIsDirty, setSQLCodeEditorIsDirty] = useState(false);
|
||||
const migratedQuery = useMigratedMetricsQuery(query, props.onChange);
|
||||
|
||||
const onChange = (query: CloudWatchQuery) => {
|
||||
const { onChange, onRunQuery } = props;
|
||||
onChange(query);
|
||||
onRunQuery();
|
||||
};
|
||||
const onEditorModeChange = useCallback(
|
||||
(newMetricEditorMode: MetricEditorMode) => {
|
||||
if (
|
||||
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 (
|
||||
<>
|
||||
<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} />
|
||||
|
||||
{query.metricQueryType === MetricQueryType.Search && (
|
||||
@ -65,7 +124,6 @@ export const MetricsQueryEditor = (props: Props) => {
|
||||
)}
|
||||
{query.metricEditorMode === MetricEditorMode.Code && (
|
||||
<MathExpressionQueryField
|
||||
onRunQuery={onRunQuery}
|
||||
expression={query.expression ?? ''}
|
||||
onChange={(expression) => props.onChange({ ...query, expression })}
|
||||
datasource={datasource}
|
||||
@ -85,19 +143,13 @@ export const MetricsQueryEditor = (props: Props) => {
|
||||
}
|
||||
props.onChange({ ...migratedQuery, sqlExpression });
|
||||
}}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
/>
|
||||
)}
|
||||
|
||||
{query.metricEditorMode === MetricEditorMode.Builder && (
|
||||
<>
|
||||
<SQLBuilderEditor
|
||||
query={query}
|
||||
onChange={props.onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
></SQLBuilderEditor>
|
||||
<SQLBuilderEditor query={query} onChange={props.onChange} datasource={datasource}></SQLBuilderEditor>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -113,7 +165,6 @@ export const MetricsQueryEditor = (props: Props) => {
|
||||
>
|
||||
<Input
|
||||
id={`${query.refId}-cloudwatch-metric-query-editor-id`}
|
||||
onBlur={onRunQuery}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange({ ...migratedQuery, id: event.target.value })}
|
||||
type="text"
|
||||
value={query.id}
|
||||
@ -125,7 +176,6 @@ export const MetricsQueryEditor = (props: Props) => {
|
||||
id={`${query.refId}-cloudwatch-metric-query-editor-period`}
|
||||
value={query.period || ''}
|
||||
placeholder="auto"
|
||||
onBlur={onRunQuery}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||
onChange({ ...migratedQuery, period: event.target.value })
|
||||
}
|
||||
@ -141,7 +191,6 @@ export const MetricsQueryEditor = (props: Props) => {
|
||||
>
|
||||
<DynamicLabelsField
|
||||
width={52}
|
||||
onRunQuery={onRunQuery}
|
||||
label={migratedQuery.label ?? ''}
|
||||
onChange={(label) => props.onChange({ ...query, label })}
|
||||
></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 { QueryEditorProps } from '@grafana/data';
|
||||
@ -15,7 +15,7 @@ import {
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
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
|
||||
const migratedFields = {
|
||||
@ -31,7 +31,22 @@ const props: QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJ
|
||||
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', () => {
|
||||
it('should render the metrics query editor', async () => {
|
||||
const query = {
|
||||
@ -50,7 +65,7 @@ describe('PanelQueryEditor should render right editor', () => {
|
||||
returnData: false,
|
||||
};
|
||||
await act(async () => {
|
||||
render(<PanelQueryEditor {...props} query={query} />);
|
||||
render(<QueryEditor {...props} query={query} />);
|
||||
});
|
||||
expect(screen.getByText('Metric name')).toBeInTheDocument();
|
||||
});
|
||||
@ -78,7 +93,7 @@ describe('PanelQueryEditor should render right editor', () => {
|
||||
statistics: 'Average',
|
||||
} as any;
|
||||
await act(async () => {
|
||||
render(<PanelQueryEditor {...props} query={query} />);
|
||||
render(<QueryEditor {...props} query={query} />);
|
||||
});
|
||||
expect(screen.getByText('Choose Log Groups')).toBeInTheDocument();
|
||||
});
|
||||
@ -106,7 +121,7 @@ describe('PanelQueryEditor should render right editor', () => {
|
||||
statistic: 'Average',
|
||||
} as any;
|
||||
await act(async () => {
|
||||
render(<PanelQueryEditor {...props} query={query} />);
|
||||
render(<QueryEditor {...props} query={query} />);
|
||||
});
|
||||
expect(screen.getByText('Log Groups')).toBeInTheDocument();
|
||||
});
|
||||
@ -133,7 +148,7 @@ describe('PanelQueryEditor should render right editor', () => {
|
||||
statistic: 'Average',
|
||||
} as any;
|
||||
await act(async () => {
|
||||
render(<PanelQueryEditor {...props} query={query} />);
|
||||
render(<QueryEditor {...props} query={query} />);
|
||||
});
|
||||
expect(screen.getByText('Metric name')).toBeInTheDocument();
|
||||
});
|
||||
@ -177,7 +192,7 @@ describe('PanelQueryEditor should render right editor', () => {
|
||||
test.each(cases)('$name', async ({ query, toggle }) => {
|
||||
config.featureToggles.cloudWatchCrossAccountQuerying = toggle;
|
||||
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();
|
||||
});
|
||||
@ -193,7 +208,7 @@ describe('PanelQueryEditor should render right editor', () => {
|
||||
{
|
||||
name: 'it is metric query code query and toggle is not enabled',
|
||||
query: validMetricQueryCodeQuery,
|
||||
toggle: true,
|
||||
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 }) => {
|
||||
config.featureToggles.cloudWatchCrossAccountQuerying = toggle;
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 { validLogsQuery, validMetricSearchBuilderQuery } from '../__mocks__/queries';
|
||||
import { CloudWatchLogsQuery, CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types';
|
||||
|
||||
import QueryHeader from './QueryHeader';
|
||||
|
||||
@ -20,74 +19,6 @@ describe('QueryHeader', () => {
|
||||
afterEach(() => {
|
||||
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', () => {
|
||||
const { datasource } = setupMockedDataSource();
|
||||
@ -101,11 +32,11 @@ describe('QueryHeader', () => {
|
||||
datasource.api.isMonitoringAccount = jest.fn().mockResolvedValue(false);
|
||||
render(
|
||||
<QueryHeader
|
||||
sqlCodeEditorIsDirty={true}
|
||||
datasource={datasource}
|
||||
query={{ ...validMetricSearchBuilderQuery, region: 'us-east-1', accountId: 'all' }}
|
||||
onChange={onChange}
|
||||
onRunQuery={jest.fn()}
|
||||
dataIsStale={false}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => expect(screen.queryByText('us-east-1')).toBeInTheDocument());
|
||||
@ -126,11 +57,11 @@ describe('QueryHeader', () => {
|
||||
|
||||
render(
|
||||
<QueryHeader
|
||||
sqlCodeEditorIsDirty={true}
|
||||
datasource={datasource}
|
||||
query={{ ...validMetricSearchBuilderQuery, region: 'us-east-1', accountId: '123' }}
|
||||
onChange={onChange}
|
||||
onRunQuery={jest.fn()}
|
||||
dataIsStale={false}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => expect(screen.queryByText('us-east-1')).toBeInTheDocument());
|
||||
@ -151,7 +82,7 @@ describe('QueryHeader', () => {
|
||||
|
||||
render(
|
||||
<QueryHeader
|
||||
sqlCodeEditorIsDirty={true}
|
||||
dataIsStale={false}
|
||||
datasource={datasource}
|
||||
query={{ ...validLogsQuery, region: 'us-east-1' }}
|
||||
onChange={onChange}
|
||||
@ -172,7 +103,7 @@ describe('QueryHeader', () => {
|
||||
|
||||
render(
|
||||
<QueryHeader
|
||||
sqlCodeEditorIsDirty={true}
|
||||
dataIsStale={false}
|
||||
datasource={datasource}
|
||||
query={{ ...validLogsQuery, region: 'us-east-1' }}
|
||||
onChange={onChange}
|
||||
|
@ -1,23 +1,19 @@
|
||||
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 { config } from '@grafana/runtime';
|
||||
import { Badge } from '@grafana/ui';
|
||||
import { Badge, Button } from '@grafana/ui';
|
||||
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import { isCloudWatchMetricsQuery } from '../guards';
|
||||
import { useIsMonitoringAccount, useRegions } from '../hooks';
|
||||
import { CloudWatchQuery, CloudWatchQueryMode } from '../types';
|
||||
import { CloudWatchJsonData, CloudWatchQuery, CloudWatchQueryMode, MetricQueryType } from '../types';
|
||||
|
||||
import MetricsQueryHeader from './MetricsQueryEditor/MetricsQueryHeader';
|
||||
|
||||
interface QueryHeaderProps {
|
||||
query: CloudWatchQuery;
|
||||
datasource: CloudWatchDatasource;
|
||||
onChange: (query: CloudWatchQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
sqlCodeEditorIsDirty: boolean;
|
||||
export interface Props extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> {
|
||||
extraHeaderElementLeft?: JSX.Element;
|
||||
extraHeaderElementRight?: JSX.Element;
|
||||
dataIsStale: boolean;
|
||||
}
|
||||
|
||||
const apiModes: Array<SelectableValue<CloudWatchQueryMode>> = [
|
||||
@ -25,18 +21,27 @@ const apiModes: Array<SelectableValue<CloudWatchQueryMode>> = [
|
||||
{ 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 isMonitoringAccount = useIsMonitoringAccount(datasource.api, query.region);
|
||||
|
||||
const [regions, regionIsLoading] = useRegions(datasource);
|
||||
|
||||
const onQueryModeChange = ({ value }: SelectableValue<CloudWatchQueryMode>) => {
|
||||
if (value !== queryMode) {
|
||||
if (value && value !== queryMode) {
|
||||
onChange({
|
||||
...datasource.getDefaultQuery(CoreApp.Unknown),
|
||||
...query,
|
||||
queryMode: value,
|
||||
} as CloudWatchQuery);
|
||||
});
|
||||
}
|
||||
};
|
||||
const onRegionChange = async (region: string) => {
|
||||
@ -49,44 +54,60 @@ const QueryHeader: React.FC<QueryHeaderProps> = ({ query, sqlCodeEditorIsDirty,
|
||||
};
|
||||
|
||||
const shouldDisplayMonitoringBadge =
|
||||
queryMode === 'Logs' && isMonitoringAccount && config.featureToggles.cloudWatchCrossAccountQuerying;
|
||||
config.featureToggles.cloudWatchCrossAccountQuerying &&
|
||||
isMonitoringAccount &&
|
||||
(query.queryMode === 'Logs' ||
|
||||
(isCloudWatchMetricsQuery(query) && query.metricQueryType === MetricQueryType.Search));
|
||||
|
||||
return (
|
||||
<EditorHeader>
|
||||
<InlineSelect
|
||||
label="Region"
|
||||
value={region}
|
||||
placeholder="Select region"
|
||||
allowCustomValue
|
||||
onChange={({ value: region }) => region && onRegionChange(region)}
|
||||
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
|
||||
label="Region"
|
||||
value={region}
|
||||
placeholder="Select region"
|
||||
allowCustomValue
|
||||
onChange={({ value: region }) => region && onRegionChange(region)}
|
||||
options={regions}
|
||||
isLoading={regionIsLoading}
|
||||
/>
|
||||
)}
|
||||
</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(),
|
||||
datasource,
|
||||
onChange: () => {},
|
||||
onRunQuery: () => {},
|
||||
};
|
||||
|
||||
it('Displays the namespace', async () => {
|
||||
|
@ -17,10 +17,9 @@ export type Props = {
|
||||
query: CloudWatchMetricsQuery;
|
||||
datasource: CloudWatchDatasource;
|
||||
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 onQueryChange = useCallback(
|
||||
@ -33,9 +32,8 @@ export function SQLBuilderEditor({ query, datasource, onChange, onRunQuery }: Re
|
||||
};
|
||||
|
||||
onChange(fullQuery);
|
||||
onRunQuery();
|
||||
},
|
||||
[onChange, onRunQuery]
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const [sqlPreview, setSQLPreview] = useState<string | undefined>();
|
||||
|
@ -12,11 +12,10 @@ export interface Props {
|
||||
region: string;
|
||||
sql: string;
|
||||
onChange: (sql: string) => void;
|
||||
onRunQuery: () => void;
|
||||
datasource: CloudWatchDatasource;
|
||||
}
|
||||
|
||||
export const SQLCodeEditor: FunctionComponent<Props> = ({ region, sql, onChange, onRunQuery, datasource }) => {
|
||||
export const SQLCodeEditor: FunctionComponent<Props> = ({ region, sql, onChange, datasource }) => {
|
||||
useEffect(() => {
|
||||
datasource.sqlCompletionItemProvider.setRegion(region);
|
||||
}, [region, datasource]);
|
||||
@ -27,10 +26,9 @@ export const SQLCodeEditor: FunctionComponent<Props> = ({ region, sql, onChange,
|
||||
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {
|
||||
const text = editor.getValue();
|
||||
onChange(text);
|
||||
onRunQuery();
|
||||
});
|
||||
},
|
||||
[onChange, onRunQuery]
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -1,8 +1,7 @@
|
||||
export { Dimensions } from './Dimensions/Dimensions';
|
||||
export { QueryInlineField, QueryField } from './Forms';
|
||||
export { PanelQueryEditor } from './PanelQueryEditor';
|
||||
export { QueryEditor as PanelQueryEditor } from './QueryEditor';
|
||||
export { CloudWatchLogsQueryEditor } from './LogsQueryEditor';
|
||||
export { MetricStatEditor } from './MetricStatEditor';
|
||||
export { SQLBuilderEditor } from './SQLBuilderEditor';
|
||||
export { MathExpressionQueryField } from './MathExpressionQueryField';
|
||||
export { SQLCodeEditor } from './SQLCodeEditor';
|
||||
|
@ -159,7 +159,7 @@ export class CloudWatchDatasource
|
||||
}
|
||||
|
||||
getQueryDisplayText(query: CloudWatchQuery) {
|
||||
if (query.queryMode === 'Logs') {
|
||||
if (isCloudWatchLogsQuery(query)) {
|
||||
return query.expression ?? '';
|
||||
} else {
|
||||
return JSON.stringify(query);
|
||||
|
@ -4,7 +4,7 @@ import { getAppEvents } from '@grafana/runtime';
|
||||
import { ConfigEditor } from './components/ConfigEditor';
|
||||
import LogsCheatSheet from './components/LogsCheatSheet';
|
||||
import { MetaInspector } from './components/MetaInspector';
|
||||
import { PanelQueryEditor } from './components/PanelQueryEditor';
|
||||
import { QueryEditor } from './components/QueryEditor';
|
||||
import { CloudWatchDatasource } from './datasource';
|
||||
import { onDashboardLoadedHandler } from './tracking';
|
||||
import { CloudWatchJsonData, CloudWatchQuery } from './types';
|
||||
@ -14,7 +14,7 @@ export const plugin = new DataSourcePlugin<CloudWatchDatasource, CloudWatchQuery
|
||||
)
|
||||
.setQueryEditorHelp(LogsCheatSheet)
|
||||
.setConfigEditor(ConfigEditor)
|
||||
.setQueryEditor(PanelQueryEditor)
|
||||
.setQueryEditor(QueryEditor)
|
||||
.setMetadataInspector(MetaInspector);
|
||||
|
||||
// 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 {
|
||||
queryMode?: 'Metrics';
|
||||
queryMode?: CloudWatchQueryMode;
|
||||
metricQueryType?: MetricQueryType;
|
||||
metricEditorMode?: MetricEditorMode;
|
||||
|
||||
@ -96,7 +96,7 @@ export enum CloudWatchLogsQueryStatus {
|
||||
}
|
||||
|
||||
export interface CloudWatchLogsQuery extends DataQuery {
|
||||
queryMode: 'Logs';
|
||||
queryMode: CloudWatchQueryMode;
|
||||
id: string;
|
||||
region: string;
|
||||
expression?: string;
|
||||
@ -115,7 +115,7 @@ export type CloudWatchQuery =
|
||||
| CloudWatchDefaultQuery;
|
||||
|
||||
export interface CloudWatchAnnotationQuery extends MetricStat, DataQuery {
|
||||
queryMode: 'Annotations';
|
||||
queryMode: CloudWatchQueryMode;
|
||||
prefixMatching?: boolean;
|
||||
actionPrefix?: string;
|
||||
alarmNamePrefix?: string;
|
||||
|
Loading…
Reference in New Issue
Block a user