Commit c600a085 by Boyko Committed by GitHub

Query components unsafe lifecycle methods (#21163)

* Chore: refactor query components unsafe lifecycle methods

* Chore: remove redundant defaults

* Chore: check expression exist while filter targets in query

* Chore: fix broken test

* Chore: adding query filed tests

* pass normalized query to QueryFieldsEditor

Signed-off-by: Boyko Lalov <boiskila@gmail.com>

* Remove introduced strictNullCheck errors and correctly solve merged conficts

* Improve redability and fix strictNullChecks

Co-authored-by: Ivana <ivana.huckova@gmail.com>
parent aea39291
...@@ -49,4 +49,36 @@ describe('<QueryField />', () => { ...@@ -49,4 +49,36 @@ describe('<QueryField />', () => {
expect(onBlur.mock.calls.length).toBe(1); expect(onBlur.mock.calls.length).toBe(1);
expect(onRun.mock.calls.length).toBe(0); expect(onRun.mock.calls.length).toBe(0);
}); });
describe('syntaxLoaded', () => {
it('should re-render the editor after syntax has fully loaded', () => {
const wrapper: any = shallow(<QueryField query="my query" portalOrigin="mock-origin" />);
const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn());
wrapper.instance().editor = { insertText: () => ({ deleteBackward: () => ({ value: 'fooo' }) }) };
wrapper.setProps({ syntaxLoaded: true });
expect(spyOnChange).toHaveBeenCalledWith('fooo', true);
});
it('should not re-render the editor if syntax is already loaded', () => {
const wrapper: any = shallow(<QueryField query="my query" portalOrigin="mock-origin" />);
const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn());
wrapper.setProps({ syntaxLoaded: true });
wrapper.instance().editor = {};
wrapper.setProps({ syntaxLoaded: true });
expect(spyOnChange).not.toBeCalled();
});
it('should not re-render the editor if editor itself is not defined', () => {
const wrapper: any = shallow(<QueryField query="my query" portalOrigin="mock-origin" />);
const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn());
wrapper.setProps({ syntaxLoaded: true });
expect(wrapper.instance().editor).toBeFalsy();
expect(spyOnChange).not.toBeCalled();
});
it('should not re-render the editor twice once syntax is fully lodaded', () => {
const wrapper: any = shallow(<QueryField query="my query" portalOrigin="mock-origin" />);
const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn());
wrapper.instance().editor = { insertText: () => ({ deleteBackward: () => ({ value: 'fooo' }) }) };
wrapper.setProps({ syntaxLoaded: true });
wrapper.setProps({ syntaxLoaded: true });
expect(spyOnChange).toBeCalledTimes(1);
});
});
}); });
...@@ -95,7 +95,13 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS ...@@ -95,7 +95,13 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
} }
componentDidUpdate(prevProps: QueryFieldProps, prevState: QueryFieldState) { componentDidUpdate(prevProps: QueryFieldProps, prevState: QueryFieldState) {
const { query, syntax } = this.props; const { query, syntax, syntaxLoaded } = this.props;
if (!prevProps.syntaxLoaded && syntaxLoaded && this.editor) {
// Need a bogus edit to re-render the editor after syntax has fully loaded
const editor = this.editor.insertText(' ').deleteBackward(1);
this.onChange(editor.value, true);
}
const { value } = this.state; const { value } = this.state;
// Handle two way binging between local state and outside prop. // Handle two way binging between local state and outside prop.
...@@ -108,18 +114,6 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS ...@@ -108,18 +114,6 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
} }
} }
UNSAFE_componentWillReceiveProps(nextProps: QueryFieldProps) {
if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) {
if (!this.editor) {
return;
}
// Need a bogus edit to re-render the editor after syntax has fully loaded
const editor = this.editor.insertText(' ').deleteBackward(1);
this.onChange(editor.value, true);
}
}
/** /**
* Update local state, propagate change upstream and optionally run the query afterwards. * Update local state, propagate change upstream and optionally run the query afterwards.
*/ */
......
...@@ -5,7 +5,7 @@ import { act } from 'react-dom/test-utils'; ...@@ -5,7 +5,7 @@ import { act } from 'react-dom/test-utils';
import { DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceInstanceSettings } from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { CustomVariable } from 'app/features/templating/all'; import { CustomVariable } from 'app/features/templating/all';
import { Props, QueryEditor } from './QueryEditor'; import { Props, QueryEditor, normalizeQuery } from './QueryEditor';
import CloudWatchDatasource from '../datasource'; import CloudWatchDatasource from '../datasource';
const setup = () => { const setup = () => {
...@@ -86,26 +86,19 @@ describe('QueryEditor', () => { ...@@ -86,26 +86,19 @@ describe('QueryEditor', () => {
}); });
}); });
it('should init props correctly', async () => { it('should normalize query with default values', () => {
// @ts-ignore strict null error TS2345: Argument of type '() => Promise<void>' is not assignable to parameter of type '() => void | undefined'. expect(normalizeQuery({ refId: '42' } as any)).toEqual({
await act(async () => { namespace: '',
const props = setup(); metricName: '',
props.query.namespace = (null as unknown) as string; expression: '',
props.query.metricName = (null as unknown) as string; dimensions: {},
props.query.expression = (null as unknown) as string; region: 'default',
props.query.dimensions = (null as unknown) as { [key: string]: string | string[] }; id: '',
props.query.region = (null as unknown) as string; alias: '',
props.query.statistics = (null as unknown) as string[]; statistics: ['Average'],
const wrapper = mount(<QueryEditor {...props} />); matchExact: true,
const { period: '',
query: { namespace, region, metricName, dimensions, statistics, expression }, refId: '42',
} = wrapper.props();
expect(namespace).toEqual('');
expect(metricName).toEqual('');
expect(expression).toEqual('');
expect(region).toEqual('default');
expect(statistics).toEqual(['Average']);
expect(dimensions).toEqual({});
}); });
}); });
}); });
......
import React, { PureComponent, ChangeEvent } from 'react'; import React, { PureComponent, ChangeEvent } from 'react';
import { ExploreQueryFieldProps } from '@grafana/data'; import { ExploreQueryFieldProps } from '@grafana/data';
import { Input, ValidationEvents, EventsWithValidation, Switch } from '@grafana/ui'; import { Input, ValidationEvents, EventsWithValidation, Switch } from '@grafana/ui';
import isEmpty from 'lodash/isEmpty';
import { CloudWatchQuery } from '../types'; import { CloudWatchQuery } from '../types';
import CloudWatchDatasource from '../datasource'; import CloudWatchDatasource from '../datasource';
import { QueryField, Alias, QueryFieldsEditor } from './'; import { QueryField, Alias, QueryFieldsEditor } from './';
...@@ -20,51 +21,36 @@ const idValidationEvents: ValidationEvents = { ...@@ -20,51 +21,36 @@ const idValidationEvents: ValidationEvents = {
], ],
}; };
export const normalizeQuery = ({
namespace,
metricName,
expression,
dimensions,
region,
id,
alias,
statistics,
period,
...rest
}: CloudWatchQuery): CloudWatchQuery => {
const normalizedQuery = {
namespace: namespace || '',
metricName: metricName || '',
expression: expression || '',
dimensions: dimensions || {},
region: region || 'default',
id: id || '',
alias: alias || '',
statistics: isEmpty(statistics) ? ['Average'] : statistics,
period: period || '',
...rest,
};
return !rest.hasOwnProperty('matchExact') ? { ...normalizedQuery, matchExact: true } : normalizedQuery;
};
export class QueryEditor extends PureComponent<Props, State> { export class QueryEditor extends PureComponent<Props, State> {
state: State = { showMeta: false }; state: State = { showMeta: false };
static getDerivedStateFromProps(props: Props, state: State) {
const { query } = props;
if (!query.namespace) {
query.namespace = '';
}
if (!query.metricName) {
query.metricName = '';
}
if (!query.expression) {
query.expression = '';
}
if (!query.dimensions) {
query.dimensions = {};
}
if (!query.region) {
query.region = 'default';
}
if (!query.id) {
query.id = '';
}
if (!query.alias) {
query.alias = '';
}
if (!query.statistics || !query.statistics.length) {
query.statistics = ['Average'];
}
if (!query.hasOwnProperty('matchExact')) {
query.matchExact = true;
}
return state;
}
onChange(query: CloudWatchQuery) { onChange(query: CloudWatchQuery) {
const { onChange, onRunQuery } = this.props; const { onChange, onRunQuery } = this.props;
onChange(query); onChange(query);
...@@ -72,12 +58,13 @@ export class QueryEditor extends PureComponent<Props, State> { ...@@ -72,12 +58,13 @@ export class QueryEditor extends PureComponent<Props, State> {
} }
render() { render() {
const { data, query, onRunQuery } = this.props; const { data, onRunQuery } = this.props;
const { showMeta } = this.state; const { showMeta } = this.state;
const query = normalizeQuery(this.props.query);
const metaDataExist = data && Object.values(data).length && data.state === 'Done'; const metaDataExist = data && Object.values(data).length && data.state === 'Done';
return ( return (
<> <>
<QueryFieldsEditor {...this.props}></QueryFieldsEditor> <QueryFieldsEditor {...{ ...this.props, query }}></QueryFieldsEditor>
{query.statistics.length <= 1 && ( {query.statistics.length <= 1 && (
<div className="gf-form-inline"> <div className="gf-form-inline">
<div className="gf-form"> <div className="gf-form">
...@@ -92,7 +79,7 @@ export class QueryEditor extends PureComponent<Props, State> { ...@@ -92,7 +79,7 @@ export class QueryEditor extends PureComponent<Props, State> {
this.onChange({ ...query, id: event.target.value }) this.onChange({ ...query, id: event.target.value })
} }
validationEvents={idValidationEvents} validationEvents={idValidationEvents}
value={query.id || ''} value={query.id}
/> />
</QueryField> </QueryField>
</div> </div>
...@@ -105,7 +92,7 @@ export class QueryEditor extends PureComponent<Props, State> { ...@@ -105,7 +92,7 @@ export class QueryEditor extends PureComponent<Props, State> {
<Input <Input
className="gf-form-input" className="gf-form-input"
onBlur={onRunQuery} onBlur={onRunQuery}
value={query.expression || ''} value={query.expression}
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange={(event: ChangeEvent<HTMLInputElement>) =>
this.onChange({ ...query, expression: event.target.value }) this.onChange({ ...query, expression: event.target.value })
} }
...@@ -119,7 +106,7 @@ export class QueryEditor extends PureComponent<Props, State> { ...@@ -119,7 +106,7 @@ export class QueryEditor extends PureComponent<Props, State> {
<QueryField label="Period" tooltip="Minimum interval between points in seconds"> <QueryField label="Period" tooltip="Minimum interval between points in seconds">
<Input <Input
className="gf-form-input width-8" className="gf-form-input width-8"
value={query.period || ''} value={query.period}
placeholder="auto" placeholder="auto"
onBlur={onRunQuery} onBlur={onRunQuery}
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange={(event: ChangeEvent<HTMLInputElement>) =>
...@@ -170,7 +157,7 @@ export class QueryEditor extends PureComponent<Props, State> { ...@@ -170,7 +157,7 @@ export class QueryEditor extends PureComponent<Props, State> {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.series[0].meta.gmdMeta.map(({ ID, Expression, Period }: any) => ( {data?.series[0]?.meta?.gmdMeta.map(({ ID, Expression, Period }: any) => (
<tr key={ID}> <tr key={ID}>
<td>{ID}</td> <td>{ID}</td>
<td>{Expression}</td> <td>{Expression}</td>
......
...@@ -68,7 +68,7 @@ export default class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, ...@@ -68,7 +68,7 @@ export default class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery,
return ( return (
(item.id !== '' || item.hide !== true) && (item.id !== '' || item.hide !== true) &&
((!!item.region && !!item.namespace && !!item.metricName && !_.isEmpty(item.statistics)) || ((!!item.region && !!item.namespace && !!item.metricName && !_.isEmpty(item.statistics)) ||
item.expression.length > 0) item.expression?.length > 0)
); );
}).map(item => { }).map(item => {
item.region = this.replace(this.getActualRegion(item.region), options.scopedVars, true, 'region'); item.region = this.replace(this.getActualRegion(item.region), options.scopedVars, true, 'region');
...@@ -112,8 +112,8 @@ export default class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, ...@@ -112,8 +112,8 @@ export default class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery,
} }
const request = { const request = {
from: options.range.from.valueOf().toString(), from: options?.range?.from.valueOf().toString(),
to: options.range.to.valueOf().toString(), to: options?.range?.to.valueOf().toString(),
queries: queries, queries: queries,
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment