Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Elasticsearch: Add custom query variable editor #16697

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/grafana-ui/src/components/Select/Select.tsx
Expand Up @@ -28,6 +28,11 @@ export interface SelectOptionItem<T> {
[key: string]: any;
}

export interface GroupedSelectOptionItem<T> extends SelectOptionItem<T> {
expanded?: boolean;
options?: Array<SelectOptionItem<T>>;
}

export interface CommonProps<T> {
defaultValue?: any;
getOptionLabel?: (item: SelectOptionItem<T>) => string;
Expand Down
22 changes: 11 additions & 11 deletions public/app/core/components/Select/MetricSelect.tsx
@@ -1,24 +1,24 @@
import React from 'react';
import _ from 'lodash';

import { Select, SelectOptionItem } from '@grafana/ui';
import { Select, GroupedSelectOptionItem } from '@grafana/ui/src/components/Select/Select';
import { Variable } from 'app/types/templates';

export interface Props {
onChange: (value: string) => void;
options: Array<SelectOptionItem<string>>;
export interface Props<T> {
onChange: (value: T) => void;
options: Array<GroupedSelectOptionItem<T>>;
isSearchable: boolean;
value: string;
value: T;
placeholder?: string;
className?: string;
variables?: Variable[];
}

interface State {
options: Array<SelectOptionItem<string>>;
interface State<T> {
options: Array<GroupedSelectOptionItem<T>>;
}

export class MetricSelect extends React.Component<Props, State> {
export class MetricSelect<T> extends React.Component<Props<T>, State<T>> {
static defaultProps = {
variables: [],
options: [],
Expand All @@ -34,13 +34,13 @@ export class MetricSelect extends React.Component<Props, State> {
this.setState({ options: this.buildOptions(this.props) });
}

componentWillReceiveProps(nextProps: Props) {
componentWillReceiveProps(nextProps: Props<T>) {
if (nextProps.options.length > 0 || nextProps.variables.length) {
this.setState({ options: this.buildOptions(nextProps) });
}
}

shouldComponentUpdate(nextProps: Props) {
shouldComponentUpdate(nextProps: Props<T>) {
const nextOptions = this.buildOptions(nextProps);
return nextProps.value !== this.props.value || !_.isEqual(nextOptions, this.state.options);
}
Expand All @@ -61,7 +61,7 @@ export class MetricSelect extends React.Component<Props, State> {

getSelectedOption() {
const { options } = this.state;
const allOptions = options.every(o => o.options) ? _.flatten(options.map(o => o.options)) : options;
const allOptions = options.every(o => o.options.length > 0) ? _.flatten(options.map(o => o.options)) : options;
return allOptions.find(option => option.value === this.props.value);
}

Expand Down
2 changes: 2 additions & 0 deletions public/app/features/templating/editor_ctrl.ts
Expand Up @@ -155,6 +155,8 @@ export class VariableEditorCtrl {
return { text: ds.meta.name, value: ds.meta.id };
})
.value();

$scope.currentDatasource = null;
marefr marked this conversation as resolved.
Show resolved Hide resolved
};

$scope.typeChanged = function() {
Expand Down
@@ -0,0 +1,72 @@
import React from 'react';
import { shallow } from 'enzyme';
import { FieldsQueryForm } from './FieldsQueryForm';
import { FieldTypes } from '../types';

const setup = (propOverrides?: object) => {
const props = {
onChange: () => {},
query: {},
...propOverrides,
};

const wrapper = shallow(<FieldsQueryForm {...props} />);
const instance = wrapper.instance() as FieldsQueryForm;

return {
wrapper,
instance,
};
};

describe('FieldsQueryForm', () => {
it('should render component', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
});

it('when loading component with type should not trigger change event', () => {
const props = {
onChange: jest.fn(),
query: { type: 'keyword' },
};
setup(props);
expect(props.onChange.mock.calls.length).toBe(0);
});

it('when loading component without type should trigger change event', () => {
const props = {
onChange: jest.fn(),
};
setup(props);
expect(props.onChange.mock.calls.length).toBe(1);
expect(props.onChange.mock.calls[0][0].find).toBe('fields');
expect(props.onChange.mock.calls[0][1]).toBe('Fields(Any)');
});

it('when changing type should trigger change event', () => {
const props = {
onChange: jest.fn(),
};
const { instance } = setup(props);
props.onChange.mockReset();
instance.onFieldTypeChange(FieldTypes.Keyword);
expect(props.onChange.mock.calls.length).toBe(1);
expect(props.onChange.mock.calls[0][0].find).toBe('fields');
expect(props.onChange.mock.calls[0][0].type).toBe(FieldTypes.Keyword);
expect(props.onChange.mock.calls[0][1]).toBe('Fields(Keyword)');
});

it('when changing type to template variable should trigger change event', () => {
const props = {
onChange: jest.fn(),
};
const { instance } = setup(props);
props.onChange.mockReset();
instance.onFieldTypeChange('$var');
expect(props.onChange.mock.calls.length).toBe(1);
expect(props.onChange.mock.calls[0][0].find).toBe('fields');
expect(props.onChange.mock.calls[0][0].type).toBe('$var');
expect(props.onChange.mock.calls[0][1]).toBe('Fields($var)');
});
});
@@ -0,0 +1,95 @@
import React, { PureComponent } from 'react';
import { FormLabel } from '@grafana/ui';
import { FieldTypes } from '../types';
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
import { Variable } from 'app/types/templates';
import { GroupedSelectOptionItem } from '@grafana/ui/src/components/Select/Select';

export interface FieldsQueryFormProps {
query: any;
variables?: Variable[];
onChange: (query: any, definition: string) => void;
}

export interface FieldsQueryFormState {
type: string;
}

const defaultState: FieldsQueryFormState = {
type: '',
};

const fieldTypes: GroupedSelectOptionItem<FieldTypes> = {
label: 'Field types',
options: [
{ value: FieldTypes.Any, label: 'Any' },
{ value: FieldTypes.Number, label: 'Number' },
{ value: FieldTypes.Date, label: 'Date' },
{ value: FieldTypes.String, label: 'String' },
{ value: FieldTypes.Keyword, label: 'Keyword' },
{ value: FieldTypes.Nested, label: 'Nested' },
],
};

export class FieldsQueryForm extends PureComponent<FieldsQueryFormProps, FieldsQueryFormState> {
constructor(props: FieldsQueryFormProps) {
super(props);
this.state = {
...defaultState,
...props.query,
};
}

componentDidMount() {
if (!this.props.query.type) {
this.triggerChange();
}
}

triggerChange() {
const { onChange } = this.props;
const { type } = this.state;
const query: any = {
find: 'fields',
};
const selectedtype = fieldTypes.options.find(o => o.value === type);

if (defaultState.type !== type) {
query.type = type;
}

onChange(query, `Fields(${selectedtype ? selectedtype.label : type})`);
}

onFieldTypeChange = (type: string) => {
this.setState(
{
type,
},
() => this.triggerChange()
);
};

render() {
const { variables } = this.props;
const { type } = this.state;
fieldTypes.expanded = fieldTypes.options.some(o => o.value === type);

return (
<>
<div className="form-field">
<FormLabel>Field Type</FormLabel>
<MetricSelect
placeholder="Select field type"
isSearchable={true}
options={[fieldTypes]}
value={type}
onChange={this.onFieldTypeChange}
variables={variables}
className="width-15"
/>
</div>
</>
);
}
}
@@ -0,0 +1,89 @@
import React from 'react';
import { shallow } from 'enzyme';
import { TermsQueryForm } from './TermsQueryForm';

const setup = (propOverrides?: object) => {
const props = {
onChange: () => {},
query: {},
fields: [{ value: 'field.a', label: 'field.a', description: 'number' }],
...propOverrides,
};

const wrapper = shallow(<TermsQueryForm {...props} />);
const instance = wrapper.instance() as TermsQueryForm;

return {
wrapper,
instance,
};
};

describe('TermsQueryForm', () => {
it('should render component', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
});

it('when loading component should not trigger change event', () => {
const props = {
onChange: jest.fn(),
};
setup(props);
expect(props.onChange.mock.calls.length).toBe(0);
});

it('when changing field should trigger change event', () => {
const props = {
onChange: jest.fn(),
};
const { instance } = setup(props);
instance.onFieldChange('field.a');
expect(props.onChange.mock.calls.length).toBe(1);
expect(props.onChange.mock.calls[0][0].find).toBe('terms');
expect(props.onChange.mock.calls[0][0].field).toBe('field.a');
expect(props.onChange.mock.calls[0][1]).toBe('Terms(field.a)');
});

it('when changing query should trigger change event', () => {
const props = {
onChange: jest.fn(),
query: { field: 'field.a' },
};
const { instance } = setup(props);
instance.onQueryChange({
target: {
value: 'field.a:*',
},
} as any);
instance.onQueryBlur();
expect(props.onChange.mock.calls.length).toBe(1);
expect(props.onChange.mock.calls[0][0]).toEqual({
find: 'terms',
field: 'field.a',
query: 'field.a:*',
});
expect(props.onChange.mock.calls[0][1]).toBe('Terms(field.a)');
});

it('when changing size should trigger change event', () => {
const props = {
onChange: jest.fn(),
query: { field: 'field.a' },
};
const { instance } = setup(props);
instance.onSizeChange({
target: {
value: '1000',
},
} as any);
instance.onSizeBlur();
expect(props.onChange.mock.calls.length).toBe(1);
expect(props.onChange.mock.calls[0][0]).toEqual({
find: 'terms',
field: 'field.a',
size: 1000,
});
expect(props.onChange.mock.calls[0][1]).toBe('Terms(field.a)');
});
});