mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
CloudWatch: Use metrics query header for logs (#44668)
Co-authored-by: Sarah Zinger <sarahzinger@users.noreply.github.com> Co-authored-by: Yaelle Chaudy <42030685+yaelleC@users.noreply.github.com>
This commit is contained in:
parent
3749695594
commit
55a9c98ae3
@ -42,9 +42,7 @@ export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onBlur={() => {}}
|
||||
onChange={(val: CloudWatchQuery) => {
|
||||
onChange({ ...val, queryMode: 'Logs' });
|
||||
}}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
history={[]}
|
||||
data={data}
|
||||
|
@ -49,7 +49,6 @@ describe('CloudWatchLogsQueryField', () => {
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
const getRegionSelect = () => wrapper.find({ label: 'Region' }).props().inputEl;
|
||||
const getLogGroupSelect = () => wrapper.find({ label: 'Log Groups' }).props().inputEl;
|
||||
|
||||
getLogGroupSelect().props.onChange([{ value: 'log_group_1' }]);
|
||||
@ -57,12 +56,12 @@ describe('CloudWatchLogsQueryField', () => {
|
||||
expect(getLogGroupSelect().props.value[0].value).toBe('log_group_1');
|
||||
|
||||
// We select new region where the selected log group does not exist
|
||||
await getRegionSelect().props.onChange({ value: 'region2' });
|
||||
await (wrapper.instance() as CloudWatchLogsQueryField).onRegionChange('region2');
|
||||
|
||||
// We clear the select
|
||||
expect(getLogGroupSelect().props.value.length).toBe(0);
|
||||
// Make sure we correctly updated the upstream state
|
||||
expect(onChange.mock.calls[onChange.mock.calls.length - 1][0]).toEqual({ region: 'region2', logGroupNames: [] });
|
||||
expect(onChange).toHaveBeenLastCalledWith({ logGroupNames: [] });
|
||||
});
|
||||
|
||||
it('should merge results of remote log groups search with existing results', async () => {
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
LegacyForms,
|
||||
MultiSelect,
|
||||
QueryField,
|
||||
Select,
|
||||
SlatePrism,
|
||||
TypeaheadInput,
|
||||
TypeaheadOutput,
|
||||
@ -31,6 +30,7 @@ import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { InputActionMeta } from '@grafana/ui/src/components/Select/types';
|
||||
import { getStatsGroups } from '../utils/query/getStatsGroups';
|
||||
import QueryHeader from './QueryHeader';
|
||||
|
||||
export interface CloudWatchLogsQueryFieldProps
|
||||
extends QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData> {
|
||||
@ -54,8 +54,6 @@ interface State {
|
||||
selectedLogGroups: Array<SelectableValue<string>>;
|
||||
availableLogGroups: Array<SelectableValue<string>>;
|
||||
loadingLogGroups: boolean;
|
||||
regions: Array<SelectableValue<string>>;
|
||||
selectedRegion: SelectableValue<string>;
|
||||
invalidLogGroups: boolean;
|
||||
hint:
|
||||
| {
|
||||
@ -76,15 +74,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
label: logGroup,
|
||||
})) ?? [],
|
||||
availableLogGroups: [],
|
||||
regions: [],
|
||||
invalidLogGroups: false,
|
||||
selectedRegion: (this.props.query as CloudWatchLogsQuery).region
|
||||
? {
|
||||
label: (this.props.query as CloudWatchLogsQuery).region,
|
||||
value: (this.props.query as CloudWatchLogsQuery).region,
|
||||
text: (this.props.query as CloudWatchLogsQuery).region,
|
||||
}
|
||||
: { label: 'default', value: 'default', text: 'default' },
|
||||
loadingLogGroups: false,
|
||||
hint: undefined,
|
||||
};
|
||||
@ -165,7 +155,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
onLogGroupSearchDebounced = debounce(this.onLogGroupSearch, 300);
|
||||
|
||||
componentDidMount = () => {
|
||||
const { datasource, query, onChange } = this.props;
|
||||
const { query, onChange } = this.props;
|
||||
|
||||
this.setState({
|
||||
loadingLogGroups: true,
|
||||
@ -191,25 +181,18 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
datasource.getRegions().then((regions) => {
|
||||
this.setState({
|
||||
regions,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onChangeQuery = (value: string) => {
|
||||
// Send text change to parent
|
||||
const { query, onChange } = this.props;
|
||||
const { selectedLogGroups, selectedRegion } = this.state;
|
||||
const { selectedLogGroups } = this.state;
|
||||
|
||||
if (onChange) {
|
||||
const nextQuery = {
|
||||
...query,
|
||||
expression: value,
|
||||
logGroupNames: selectedLogGroups?.map((logGroupName) => logGroupName.value!) ?? [],
|
||||
region: selectedRegion.value ?? 'default',
|
||||
statsGroups: getStatsGroups(value),
|
||||
};
|
||||
onChange(nextQuery);
|
||||
@ -234,22 +217,17 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
this.setSelectedLogGroups(selectedLogGroups);
|
||||
};
|
||||
|
||||
setSelectedRegion = async (v: SelectableValue<string>) => {
|
||||
onRegionChange = async (v: string) => {
|
||||
this.setState({
|
||||
selectedRegion: v,
|
||||
loadingLogGroups: true,
|
||||
});
|
||||
|
||||
const logGroups = await this.fetchLogGroupOptions(v.value!);
|
||||
|
||||
const logGroups = await this.fetchLogGroupOptions(v);
|
||||
this.setState((state) => {
|
||||
const selectedLogGroups = intersectionBy(state.selectedLogGroups, logGroups, 'value');
|
||||
|
||||
const { onChange, query } = this.props;
|
||||
if (onChange) {
|
||||
const nextQuery = {
|
||||
...query,
|
||||
region: v.value ?? 'default',
|
||||
logGroupNames: selectedLogGroups.map((group) => group.value!),
|
||||
};
|
||||
|
||||
@ -307,16 +285,8 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
};
|
||||
|
||||
render() {
|
||||
const { ExtraFieldElement, data, query, datasource, allowCustomValue } = this.props;
|
||||
const {
|
||||
selectedLogGroups,
|
||||
availableLogGroups,
|
||||
regions,
|
||||
selectedRegion,
|
||||
loadingLogGroups,
|
||||
hint,
|
||||
invalidLogGroups,
|
||||
} = this.state;
|
||||
const { onRunQuery, onChange, ExtraFieldElement, data, query, datasource, allowCustomValue } = this.props;
|
||||
const { selectedLogGroups, availableLogGroups, loadingLogGroups, hint, invalidLogGroups } = this.state;
|
||||
|
||||
const showError = data && data.error && data.error.refId === query.refId;
|
||||
const cleanText = datasource.languageProvider ? datasource.languageProvider.cleanText : undefined;
|
||||
@ -325,24 +295,15 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryHeader
|
||||
query={query}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
sqlCodeEditorIsDirty={false}
|
||||
onRegionChange={this.onRegionChange}
|
||||
/>
|
||||
<div className={`gf-form gf-form--grow flex-grow-1 ${rowGap}`}>
|
||||
<LegacyForms.FormField
|
||||
label="Region"
|
||||
labelWidth={4}
|
||||
inputEl={
|
||||
<Select
|
||||
aria-label="Region"
|
||||
menuShouldPortal
|
||||
options={regions}
|
||||
value={selectedRegion}
|
||||
onChange={(v) => this.setSelectedRegion(v)}
|
||||
width={18}
|
||||
placeholder="Choose Region"
|
||||
maxMenuHeight={500}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<LegacyForms.FormField
|
||||
label="Log Groups"
|
||||
labelWidth={6}
|
||||
@ -371,7 +332,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
isLoading={loadingLogGroups}
|
||||
onOpenMenu={this.onOpenLogGroupMenu}
|
||||
onInputChange={(value, actionMeta) => {
|
||||
this.onLogGroupSearchDebounced(value, selectedRegion.value ?? 'default', actionMeta);
|
||||
this.onLogGroupSearchDebounced(value, query.region, actionMeta);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
@ -0,0 +1,133 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types';
|
||||
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
|
||||
import MetricsQueryHeader from './MetricsQueryHeader';
|
||||
|
||||
const ds = setupMockedDataSource({
|
||||
variables: [],
|
||||
});
|
||||
ds.datasource.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}
|
||||
/>
|
||||
);
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
||||
const runQueryButton = screen.getByText('Run query');
|
||||
expect(runQueryButton).toBeInTheDocument();
|
||||
await act(async () => {
|
||||
await runQueryButton.click();
|
||||
});
|
||||
expect(onRunQuery).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -0,0 +1,89 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Button, ConfirmModal, RadioButtonGroup } from '@grafana/ui';
|
||||
import { InlineSelect, FlexItem } from '@grafana/experimental';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
}) => {
|
||||
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]
|
||||
);
|
||||
|
||||
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} />
|
||||
|
||||
<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,20 +1,12 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
import { QueryEditorProps, ExploreMode } from '@grafana/data';
|
||||
import { Segment } from '@grafana/ui';
|
||||
import { CloudWatchJsonData, CloudWatchQuery } from '../types';
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import { QueryInlineField } from './';
|
||||
import { MetricsQueryEditor } from './MetricsQueryEditor';
|
||||
import LogsQueryEditor from './LogsQueryEditor';
|
||||
|
||||
export type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>;
|
||||
|
||||
const apiModes = {
|
||||
Metrics: { label: 'CloudWatch Metrics', value: 'Metrics' },
|
||||
Logs: { label: 'CloudWatch Logs', value: 'Logs' },
|
||||
};
|
||||
|
||||
export class PanelQueryEditor extends PureComponent<Props> {
|
||||
render() {
|
||||
const { query } = this.props;
|
||||
@ -22,36 +14,6 @@ export class PanelQueryEditor extends PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* TODO: Remove this in favor of the QueryHeader */}
|
||||
{apiMode === ExploreMode.Logs && (
|
||||
<QueryInlineField label="Query Mode">
|
||||
<Segment
|
||||
value={apiModes[apiMode]}
|
||||
options={Object.values(apiModes)}
|
||||
onChange={({ value }) => {
|
||||
const newMode = (value as 'Metrics' | 'Logs') ?? 'Metrics';
|
||||
if (newMode !== apiModes[apiMode].value) {
|
||||
const commonProps = pick(
|
||||
query,
|
||||
'id',
|
||||
'region',
|
||||
'namespace',
|
||||
'refId',
|
||||
'hide',
|
||||
'key',
|
||||
'queryType',
|
||||
'datasource'
|
||||
);
|
||||
|
||||
this.props.onChange({
|
||||
...commonProps,
|
||||
queryMode: newMode,
|
||||
} as CloudWatchQuery);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</QueryInlineField>
|
||||
)}
|
||||
{apiMode === ExploreMode.Logs ? (
|
||||
<LogsQueryEditor {...this.props} allowCustomValue />
|
||||
) : (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types';
|
||||
import { CloudWatchLogsQuery, CloudWatchMetricsQuery, MetricEditorMode, MetricQueryType } from '../types';
|
||||
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
|
||||
import QueryHeader from './QueryHeader';
|
||||
|
||||
@ -9,105 +9,23 @@ const ds = setupMockedDataSource({
|
||||
variables: [],
|
||||
});
|
||||
ds.datasource.getRegions = jest.fn().mockResolvedValue([]);
|
||||
const query: CloudWatchMetricsQuery = {
|
||||
id: '',
|
||||
region: 'us-east-2',
|
||||
namespace: '',
|
||||
period: '',
|
||||
alias: '',
|
||||
metricName: '',
|
||||
dimensions: {},
|
||||
matchExact: true,
|
||||
statistic: '',
|
||||
expression: '',
|
||||
refId: '',
|
||||
};
|
||||
|
||||
describe('QueryHeader', () => {
|
||||
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(
|
||||
<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 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(
|
||||
<QueryHeader
|
||||
sqlCodeEditorIsDirty={true}
|
||||
datasource={ds.datasource}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<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.queryByText('Are you sure?');
|
||||
expect(modalTitleElem).toBeNull();
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('run button should be displayed in code editor in metric query mode', async () => {
|
||||
it('should display metric options for metrics', async () => {
|
||||
const query: CloudWatchMetricsQuery = {
|
||||
queryType: '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;
|
||||
@ -123,11 +41,42 @@ describe('QueryHeader', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const runQueryButton = screen.getByText('Run query');
|
||||
expect(runQueryButton).toBeInTheDocument();
|
||||
const builderElement = screen.getByLabelText('Builder');
|
||||
expect(builderElement).toBeInTheDocument();
|
||||
await act(async () => {
|
||||
await runQueryButton.click();
|
||||
await builderElement.click();
|
||||
});
|
||||
expect(onRunQuery).toHaveBeenCalled();
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
||||
const builderElement = screen.queryByLabelText('Builder');
|
||||
expect(builderElement).toBeNull();
|
||||
const codeElement = screen.queryByLabelText('Code');
|
||||
expect(codeElement).toBeNull();
|
||||
});
|
||||
});
|
||||
|
@ -1,26 +1,21 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Button, ConfirmModal, RadioButtonGroup } from '@grafana/ui';
|
||||
import { EditorHeader, InlineSelect, FlexItem } from '@grafana/experimental';
|
||||
import { ExploreMode, SelectableValue } from '@grafana/data';
|
||||
import { EditorHeader, InlineSelect } from '@grafana/experimental';
|
||||
|
||||
import { CloudWatchDatasource } from '../datasource';
|
||||
import {
|
||||
CloudWatchMetricsQuery,
|
||||
CloudWatchQuery,
|
||||
CloudWatchQueryMode,
|
||||
MetricEditorMode,
|
||||
MetricQueryType,
|
||||
} from '../types';
|
||||
import { CloudWatchQuery, CloudWatchQueryMode } from '../types';
|
||||
import { useRegions } from '../hooks';
|
||||
import MetricsQueryHeader from './MetricsQueryHeader';
|
||||
|
||||
interface QueryHeaderProps {
|
||||
query: CloudWatchMetricsQuery;
|
||||
query: CloudWatchQuery;
|
||||
datasource: CloudWatchDatasource;
|
||||
onChange: (query: CloudWatchQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
sqlCodeEditorIsDirty: boolean;
|
||||
onRegionChange?: (region: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const apiModes: Array<SelectableValue<CloudWatchQueryMode>> = [
|
||||
@ -28,48 +23,38 @@ const apiModes: Array<SelectableValue<CloudWatchQueryMode>> = [
|
||||
{ label: 'CloudWatch Logs', value: 'Logs' },
|
||||
];
|
||||
|
||||
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 QueryHeader: React.FC<QueryHeaderProps> = ({ query, sqlCodeEditorIsDirty, datasource, onChange, onRunQuery }) => {
|
||||
const { metricEditorMode, metricQueryType, queryMode, region } = query;
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const QueryHeader: React.FC<QueryHeaderProps> = ({
|
||||
query,
|
||||
sqlCodeEditorIsDirty,
|
||||
datasource,
|
||||
onChange,
|
||||
onRunQuery,
|
||||
onRegionChange,
|
||||
}) => {
|
||||
const { queryMode, region } = query;
|
||||
|
||||
const [regions, regionIsLoading] = useRegions(datasource);
|
||||
|
||||
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 onQueryModeChange = ({ value }: SelectableValue<CloudWatchQueryMode>) => {
|
||||
if (value !== queryMode) {
|
||||
const commonProps = pick(query, 'id', 'region', 'namespace', 'refId', 'hide', 'key', 'queryType', 'datasource');
|
||||
|
||||
onChange({
|
||||
...commonProps,
|
||||
queryMode: value,
|
||||
});
|
||||
} as CloudWatchQuery);
|
||||
}
|
||||
};
|
||||
|
||||
const onRegion = async ({ value }: SelectableValue<string>) => {
|
||||
if (onRegionChange) {
|
||||
await onRegionChange(value ?? 'default');
|
||||
}
|
||||
onChange({
|
||||
...query,
|
||||
region: value,
|
||||
} as CloudWatchQuery);
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorHeader>
|
||||
<InlineSelect
|
||||
@ -77,45 +62,22 @@ const QueryHeader: React.FC<QueryHeaderProps> = ({ query, sqlCodeEditorIsDirty,
|
||||
value={regions.find((v) => v.value === region)}
|
||||
placeholder="Select region"
|
||||
allowCustomValue
|
||||
onChange={({ value: region }) => region && onChange({ ...query, region: region })}
|
||||
onChange={({ value: region }) => region && onRegion({ value: region })}
|
||||
options={regions}
|
||||
isLoading={regionIsLoading}
|
||||
/>
|
||||
|
||||
<InlineSelect aria-label="Query mode" value={queryMode} options={apiModes} onChange={onQueryModeChange} />
|
||||
|
||||
<InlineSelect
|
||||
aria-label="Metric editor mode"
|
||||
value={metricEditorModes.find((m) => m.value === metricQueryType)}
|
||||
options={metricEditorModes}
|
||||
onChange={({ value }) => {
|
||||
onChange({ ...query, metricQueryType: value });
|
||||
}}
|
||||
/>
|
||||
|
||||
<FlexItem grow={1} />
|
||||
|
||||
<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>
|
||||
{queryMode !== ExploreMode.Logs && (
|
||||
<MetricsQueryHeader
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
sqlCodeEditorIsDirty={sqlCodeEditorIsDirty}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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)}
|
||||
/>
|
||||
</EditorHeader>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user