CloudMonitoring: Fix query type selection issue (#87990)
* Use deepEqual to ensure migratedQuery is only returned when query is unchanged * Add selectors for query editors * Add more tests for query editor component * Clarifying comments * Fix how state is set * Simplify query editor loading and migration
This commit is contained in:
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
|
||||
import { TextArea } from '@grafana/ui';
|
||||
|
||||
import { selectors } from '../e2e/selectors';
|
||||
|
||||
export interface Props {
|
||||
onChange: (query: string) => void;
|
||||
onRunQuery: () => void;
|
||||
@@ -17,7 +19,7 @@ export function MQLQueryEditor({ query, onChange, onRunQuery }: React.PropsWithC
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<span data-testid={selectors.components.queryEditor.mqlMetricsQueryEditor.container.input}>
|
||||
<TextArea
|
||||
name="Query"
|
||||
className="slate-query-field"
|
||||
@@ -28,6 +30,6 @@ export function MQLQueryEditor({ query, onChange, onRunQuery }: React.PropsWithC
|
||||
onChange={(e) => onChange(e.currentTarget.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { EditorField, EditorRow } from '@grafana/experimental';
|
||||
import { TextArea, Input } from '@grafana/ui';
|
||||
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { selectors } from '../e2e/selectors';
|
||||
import { PromQLQuery } from '../types/query';
|
||||
|
||||
import { Project } from './Project';
|
||||
@@ -41,7 +42,7 @@ export function PromQLQueryEditor({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span data-testid={selectors.components.queryEditor.promQlQueryEditor.container.input}>
|
||||
<EditorRow>
|
||||
<Project
|
||||
refId={refId}
|
||||
@@ -75,6 +76,6 @@ export function PromQLQueryEditor({
|
||||
/>
|
||||
</EditorField>
|
||||
</EditorRow>
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { render, waitFor, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { select } from 'react-select-event';
|
||||
|
||||
import { createMockDatasource } from '../__mocks__/cloudMonitoringDatasource';
|
||||
import { createMockQuery } from '../__mocks__/cloudMonitoringQuery';
|
||||
import { selectors } from '../e2e/selectors';
|
||||
import { QueryType } from '../types/query';
|
||||
|
||||
import { QueryEditor } from './QueryEditor';
|
||||
@@ -30,9 +32,9 @@ describe('QueryEditor', () => {
|
||||
const onChange = jest.fn();
|
||||
datasource.migrateQuery = jest.fn().mockReturnValue(defaultProps.query);
|
||||
|
||||
render(<QueryEditor {...defaultProps} datasource={datasource} onChange={onChange} />);
|
||||
render(<QueryEditor {...defaultProps} query={{ refId: 'A' }} datasource={datasource} onChange={onChange} />);
|
||||
await waitFor(() => expect(datasource.migrateQuery).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(onChange).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(onChange).toHaveBeenCalled());
|
||||
await waitFor(() => expect(onChange).toHaveBeenCalledWith(defaultProps.query));
|
||||
});
|
||||
|
||||
@@ -46,4 +48,93 @@ describe('QueryEditor', () => {
|
||||
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ queryType: QueryType.TIME_SERIES_LIST }))
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the visual metrics query editor when the query type is timeSeriesList', async () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
const mockQuery = {
|
||||
...createMockQuery(),
|
||||
queryType: QueryType.TIME_SERIES_LIST,
|
||||
};
|
||||
|
||||
render(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={() => {}} onRunQuery={() => {}} />);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByTestId(selectors.components.queryEditor.visualMetricsQueryEditor.container.input)
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
it('renders the visual metrics query editor when the query type is timeSeriesList', async () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
const mockQuery = {
|
||||
...createMockQuery(),
|
||||
queryType: QueryType.TIME_SERIES_LIST,
|
||||
};
|
||||
|
||||
render(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={() => {}} onRunQuery={() => {}} />);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByTestId(selectors.components.queryEditor.visualMetricsQueryEditor.container.input)
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
it('renders the mql metrics query editor when the query type is timeSeriesQuery', async () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
const mockQuery = {
|
||||
...createMockQuery(),
|
||||
queryType: QueryType.TIME_SERIES_QUERY,
|
||||
};
|
||||
|
||||
render(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={() => {}} onRunQuery={() => {}} />);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByTestId(selectors.components.queryEditor.mqlMetricsQueryEditor.container.input)
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the SLO query editor when the query type is SLO', async () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
const mockQuery = {
|
||||
...createMockQuery(),
|
||||
queryType: QueryType.SLO,
|
||||
};
|
||||
|
||||
render(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={() => {}} onRunQuery={() => {}} />);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId(selectors.components.queryEditor.sloQueryEditor.container.input)).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the PromQL query editor when the query type is PromQL', async () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
const mockQuery = {
|
||||
...createMockQuery(),
|
||||
queryType: QueryType.PROMQL,
|
||||
};
|
||||
|
||||
render(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={() => {}} onRunQuery={() => {}} />);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId(selectors.components.queryEditor.promQlQueryEditor.container.input)).toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
it('changes the query type when selected', async () => {
|
||||
const mockDatasource = createMockDatasource();
|
||||
const mockQuery = createMockQuery();
|
||||
const onChange = jest.fn();
|
||||
render(<QueryEditor query={mockQuery} datasource={mockDatasource} onChange={onChange} onRunQuery={() => {}} />);
|
||||
await waitFor(() => expect(screen.getByTestId(selectors.components.queryEditor.container)).toBeInTheDocument());
|
||||
|
||||
const queryType = await screen.findByLabelText(/Query type/);
|
||||
|
||||
await waitFor(() => select(queryType, 'PromQL', { container: document.body }));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
refId: mockQuery.refId,
|
||||
datasource: mockQuery.datasource,
|
||||
queryType: QueryType.PROMQL,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { isEqual } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { QueryEditorProps, getDefaultTimeRange, toOption } from '@grafana/data';
|
||||
import { EditorRows } from '@grafana/experimental';
|
||||
import { ConfirmModal } from '@grafana/ui';
|
||||
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { selectors } from '../e2e/selectors';
|
||||
import { CloudMonitoringQuery, PromQLQuery, QueryType, SLOQuery } from '../types/query';
|
||||
import { CloudMonitoringOptions } from '../types/types';
|
||||
|
||||
@@ -19,24 +21,16 @@ import { MetricQueryEditor, SLOQueryEditor } from './';
|
||||
export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery, CloudMonitoringOptions>;
|
||||
|
||||
export const QueryEditor = (props: Props) => {
|
||||
const { datasource, query: oldQ, onRunQuery, onChange, range } = props;
|
||||
const { datasource, query, onRunQuery, onChange, range } = props;
|
||||
const [modalIsOpen, setModalIsOpen] = useState<boolean>(false);
|
||||
const [migratedQuery, setMigratedQuery] = useState<CloudMonitoringQuery | undefined>();
|
||||
const query = useMemo(() => {
|
||||
if (!migratedQuery) {
|
||||
const migratedQuery = datasource.migrateQuery(oldQ);
|
||||
setMigratedQuery(migratedQuery);
|
||||
// Update the query once the migrations have been completed.
|
||||
onChange({ ...migratedQuery });
|
||||
return migratedQuery;
|
||||
}
|
||||
|
||||
if (migratedQuery) {
|
||||
return migratedQuery;
|
||||
useEffect(() => {
|
||||
const migrated = datasource.migrateQuery(query);
|
||||
if (!deepEqual(migrated, query)) {
|
||||
onChange({ ...migrated });
|
||||
}
|
||||
}, [query, datasource, onChange]);
|
||||
|
||||
return oldQ;
|
||||
}, [oldQ, datasource, onChange, migratedQuery]);
|
||||
const [currentQuery, setCurrentQuery] = useState<CloudMonitoringQuery>(query);
|
||||
const [queryHasBeenEdited, setQueryHasBeenEdited] = useState<boolean>(false);
|
||||
|
||||
@@ -95,62 +89,64 @@ export const QueryEditor = (props: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorRows>
|
||||
<ConfirmModal
|
||||
data-testid="switch-query-type-modal"
|
||||
title="Warning"
|
||||
body="By switching your query type, your current query will be lost."
|
||||
isOpen={modalIsOpen}
|
||||
onConfirm={() => {
|
||||
setModalIsOpen(false);
|
||||
onChange(currentQuery);
|
||||
setQueryHasBeenEdited(false);
|
||||
}}
|
||||
confirmText="Confirm"
|
||||
onDismiss={() => {
|
||||
setModalIsOpen(false);
|
||||
setCurrentQuery(query);
|
||||
}}
|
||||
/>
|
||||
<QueryHeader query={query} onChange={checkForModalDisplay} onRunQuery={onRunQuery} />
|
||||
|
||||
{queryType === QueryType.PROMQL && (
|
||||
<PromQLQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onPromQLQueryChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={promQLQuery}
|
||||
<span data-testid={selectors.components.queryEditor.container}>
|
||||
<EditorRows>
|
||||
<ConfirmModal
|
||||
data-testid="switch-query-type-modal"
|
||||
title="Warning"
|
||||
body="By switching your query type, your current query will be lost."
|
||||
isOpen={modalIsOpen}
|
||||
onConfirm={() => {
|
||||
setModalIsOpen(false);
|
||||
onChange(currentQuery);
|
||||
setQueryHasBeenEdited(false);
|
||||
}}
|
||||
confirmText="Confirm"
|
||||
onDismiss={() => {
|
||||
setModalIsOpen(false);
|
||||
setCurrentQuery(query);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<QueryHeader query={query} onChange={checkForModalDisplay} onRunQuery={onRunQuery} />
|
||||
|
||||
{queryType !== QueryType.SLO && (
|
||||
<MetricQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onChange={onMetricQueryChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
range={range || getDefaultTimeRange()}
|
||||
/>
|
||||
)}
|
||||
{queryType === QueryType.PROMQL && (
|
||||
<PromQLQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onChange={onPromQLQueryChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={promQLQuery}
|
||||
/>
|
||||
)}
|
||||
|
||||
{queryType === QueryType.SLO && (
|
||||
<SLOQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onChange={onSLOQueryChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={sloQuery}
|
||||
aliasBy={query.aliasBy}
|
||||
onChangeAliasBy={(aliasBy: string) => onChange({ ...query, aliasBy })}
|
||||
/>
|
||||
)}
|
||||
</EditorRows>
|
||||
{queryType !== QueryType.SLO && (
|
||||
<MetricQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onChange={onMetricQueryChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
range={range || getDefaultTimeRange()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{queryType === QueryType.SLO && (
|
||||
<SLOQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onChange={onSLOQueryChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={sloQuery}
|
||||
aliasBy={query.aliasBy}
|
||||
onChangeAliasBy={(aliasBy: string) => onChange({ ...query, aliasBy })}
|
||||
/>
|
||||
)}
|
||||
</EditorRows>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'
|
||||
|
||||
import { ALIGNMENT_PERIODS, SLO_BURN_RATE_SELECTOR_NAME } from '../constants';
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { selectors } from '../e2e/selectors';
|
||||
import { alignmentPeriodLabel } from '../functions';
|
||||
import { AlignmentTypes, SLOQuery } from '../types/query';
|
||||
import { CustomMetaData } from '../types/types';
|
||||
@@ -54,7 +55,7 @@ export function SLOQueryEditor({
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const alignmentLabel = useMemo(() => alignmentPeriodLabel(customMetaData, datasource), [customMetaData, datasource]);
|
||||
return (
|
||||
<>
|
||||
<span data-testid={selectors.components.queryEditor.sloQueryEditor.container.input}>
|
||||
<EditorRow>
|
||||
<Project
|
||||
refId={refId}
|
||||
@@ -107,6 +108,6 @@ export function SLOQueryEditor({
|
||||
|
||||
<AliasBy refId={refId} value={aliasBy} onChange={onChangeAliasBy} />
|
||||
</EditorRow>
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-2
@@ -9,6 +9,7 @@ import { reportInteraction } from '@grafana/runtime';
|
||||
import { getSelectStyles, Select, AsyncSelect, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import CloudMonitoringDatasource from '../datasource';
|
||||
import { selectors } from '../e2e/selectors';
|
||||
import { getAlignmentPickerData, getMetricType, setMetricType } from '../functions';
|
||||
import { PreprocessorType, TimeSeriesList, MetricKind, ValueTypes } from '../types/query';
|
||||
import { CustomMetaData, MetricDescriptor } from '../types/types';
|
||||
@@ -223,7 +224,7 @@ export function Editor({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<span data-testid={selectors.components.queryEditor.visualMetricsQueryEditor.container.input}>
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<Project
|
||||
@@ -306,7 +307,7 @@ export function Editor({
|
||||
<AliasBy refId={refId} value={aliasBy} onChange={onChangeAliasBy} />
|
||||
</EditorRow>
|
||||
</>
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { E2ESelectors } from '@grafana/e2e-selectors';
|
||||
|
||||
export const components = {
|
||||
queryEditor: {
|
||||
container: 'data-testid cloud-monitoring-query-editor',
|
||||
header: {
|
||||
select: 'data-testid cloud-monitoring-header',
|
||||
},
|
||||
visualMetricsQueryEditor: {
|
||||
container: { input: 'data-testid cloud-monitoring-visual-metrics-query-editor' },
|
||||
},
|
||||
mqlMetricsQueryEditor: {
|
||||
container: { input: 'data-testid cloud-monitoring-mql-query-editor' },
|
||||
},
|
||||
sloQueryEditor: {
|
||||
container: {
|
||||
input: 'data-testid cloud-monitoring-slo-query-editor',
|
||||
},
|
||||
},
|
||||
promQlQueryEditor: {
|
||||
container: {
|
||||
input: 'data-testid cloud-monitoring-prom-ql-query-editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const selectors: { components: E2ESelectors<typeof components> } = {
|
||||
components: components,
|
||||
};
|
||||
Reference in New Issue
Block a user