Shim input_control_vis for KP (#52243)
* Shim input_control_vis * Convert input_control_vis src files to typescript * Add Required, Optional, Required and Class types to kbn-utility-types * Collect all ui/* imports into legacy imports file * Pass down plugin deps from top level * Add timeout and terminate_after options to SearchSourceFields
This commit is contained in:
parent
87a9b6b6ae
commit
72c5c5cb22
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import { PromiseType } from 'utility-types';
|
||||
export { $Values, Required, Optional, Class } from 'utility-types';
|
||||
|
||||
/**
|
||||
* Returns wrapped type of a promise.
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"clean": "del target"
|
||||
},
|
||||
"dependencies": {
|
||||
"utility-types": "^3.7.0"
|
||||
"utility-types": "^3.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"del-cli": "^3.0.0",
|
||||
|
|
|
@ -61,7 +61,6 @@ export function ScriptHighlightRules() {
|
|||
},
|
||||
{
|
||||
token: 'script.keyword.operator',
|
||||
|
||||
regex:
|
||||
'\\?\\.|\\*\\.|=~|==~|!|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|->|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|typeof|void)',
|
||||
},
|
||||
|
|
44
src/legacy/core_plugins/input_control_vis/index.ts
Normal file
44
src/legacy/core_plugins/input_control_vis/index.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { Legacy } from 'kibana';
|
||||
|
||||
import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types';
|
||||
|
||||
const inputControlVisPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) =>
|
||||
new Plugin({
|
||||
id: 'input_control_vis',
|
||||
require: ['kibana', 'elasticsearch', 'visualizations', 'interpreter', 'data'],
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
uiExports: {
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
hacks: [resolve(__dirname, 'public/legacy')],
|
||||
injectDefaultVars: server => ({}),
|
||||
},
|
||||
init: (server: Legacy.Server) => ({}),
|
||||
config(Joi: any) {
|
||||
return Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
}).default();
|
||||
},
|
||||
} as Legacy.PluginSpecOptions);
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default inputControlVisPluginInitializer;
|
|
@ -16,9 +16,33 @@ exports[`renders ControlsTab 1`] = `
|
|||
"size": 5,
|
||||
"type": "terms",
|
||||
},
|
||||
"parent": "parent",
|
||||
"type": "list",
|
||||
}
|
||||
}
|
||||
deps={
|
||||
Object {
|
||||
"core": Object {
|
||||
"getStartServices": [MockFunction],
|
||||
"injectedMetadata": Object {
|
||||
"getInjectedVar": [MockFunction],
|
||||
},
|
||||
},
|
||||
"data": Object {
|
||||
"query": Object {
|
||||
"filterManager": Object {
|
||||
"fieldName": "myField",
|
||||
"getAppFilters": [MockFunction],
|
||||
"getGlobalFilters": [MockFunction],
|
||||
"getIndexPattern": [Function],
|
||||
},
|
||||
"timefilter": Object {
|
||||
"timefilter": Object {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
getIndexPattern={[Function]}
|
||||
handleCheckboxOptionChange={[Function]}
|
||||
handleFieldNameChange={[Function]}
|
||||
|
@ -49,9 +73,33 @@ exports[`renders ControlsTab 1`] = `
|
|||
"options": Object {
|
||||
"step": 1,
|
||||
},
|
||||
"parent": "parent",
|
||||
"type": "range",
|
||||
}
|
||||
}
|
||||
deps={
|
||||
Object {
|
||||
"core": Object {
|
||||
"getStartServices": [MockFunction],
|
||||
"injectedMetadata": Object {
|
||||
"getInjectedVar": [MockFunction],
|
||||
},
|
||||
},
|
||||
"data": Object {
|
||||
"query": Object {
|
||||
"filterManager": Object {
|
||||
"fieldName": "myField",
|
||||
"getAppFilters": [MockFunction],
|
||||
"getGlobalFilters": [MockFunction],
|
||||
"getIndexPattern": [Function],
|
||||
},
|
||||
"timefilter": Object {
|
||||
"timefilter": Object {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
getIndexPattern={[Function]}
|
||||
handleCheckboxOptionChange={[Function]}
|
||||
handleFieldNameChange={[Function]}
|
|
@ -3,6 +3,7 @@
|
|||
exports[`renders dynamic options should display disabled dynamic options with tooltip for non-string fields 1`] = `
|
||||
<Fragment>
|
||||
<InjectIntl(IndexPatternSelectFormRowUi)
|
||||
IndexPatternSelect={[Function]}
|
||||
controlIndex={0}
|
||||
indexPatternId="mockIndexPattern"
|
||||
onChange={[Function]}
|
||||
|
@ -116,6 +117,7 @@ exports[`renders dynamic options should display disabled dynamic options with to
|
|||
exports[`renders dynamic options should display dynamic options for string fields 1`] = `
|
||||
<Fragment>
|
||||
<InjectIntl(IndexPatternSelectFormRowUi)
|
||||
IndexPatternSelect={[Function]}
|
||||
controlIndex={0}
|
||||
indexPatternId="mockIndexPattern"
|
||||
onChange={[Function]}
|
||||
|
@ -195,6 +197,7 @@ exports[`renders dynamic options should display dynamic options for string field
|
|||
exports[`renders dynamic options should display size field when dynamic options is disabled 1`] = `
|
||||
<Fragment>
|
||||
<InjectIntl(IndexPatternSelectFormRowUi)
|
||||
IndexPatternSelect={[Function]}
|
||||
controlIndex={0}
|
||||
indexPatternId="mockIndexPattern"
|
||||
onChange={[Function]}
|
||||
|
@ -308,6 +311,7 @@ exports[`renders dynamic options should display size field when dynamic options
|
|||
exports[`renders should display chaining input when parents are provided 1`] = `
|
||||
<Fragment>
|
||||
<InjectIntl(IndexPatternSelectFormRowUi)
|
||||
IndexPatternSelect={[Function]}
|
||||
controlIndex={0}
|
||||
indexPatternId="indexPattern1"
|
||||
onChange={[Function]}
|
||||
|
@ -366,6 +370,7 @@ exports[`renders should display chaining input when parents are provided 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
value=""
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
|
@ -469,6 +474,7 @@ exports[`renders should display chaining input when parents are provided 1`] = `
|
|||
exports[`renders should not display any options until field is selected 1`] = `
|
||||
<Fragment>
|
||||
<InjectIntl(IndexPatternSelectFormRowUi)
|
||||
IndexPatternSelect={[Function]}
|
||||
controlIndex={0}
|
||||
indexPatternId="mockIndexPattern"
|
||||
onChange={[Function]}
|
|
@ -56,6 +56,7 @@ exports[`OptionsTab should renders OptionsTab 1`] = `
|
|||
labelType="label"
|
||||
>
|
||||
<EuiSwitch
|
||||
checked={false}
|
||||
data-test-subj="inputControlEditorPinFiltersCheckbox"
|
||||
label={
|
||||
<FormattedMessage
|
|
@ -3,6 +3,7 @@
|
|||
exports[`renders RangeControlEditor 1`] = `
|
||||
<Fragment>
|
||||
<InjectIntl(IndexPatternSelectFormRowUi)
|
||||
IndexPatternSelect={[Function]}
|
||||
controlIndex={0}
|
||||
indexPatternId="indexPattern1"
|
||||
onChange={[Function]}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FieldList } from 'src/plugins/data/public';
|
||||
import { InputControlVisDependencies } from '../../../plugin';
|
||||
|
||||
const fields: FieldList = [] as any;
|
||||
fields.push({ name: 'myField' } as any);
|
||||
fields.getByName = (name: any) => {
|
||||
return fields.find(({ name: n }) => n === name);
|
||||
};
|
||||
|
||||
export const getDepsMock = (): InputControlVisDependencies =>
|
||||
({
|
||||
core: {
|
||||
getStartServices: jest.fn().mockReturnValue([
|
||||
null,
|
||||
{
|
||||
data: {
|
||||
ui: {
|
||||
IndexPatternSelect: () => (<div />) as any,
|
||||
},
|
||||
indexPatterns: {
|
||||
get: () => ({
|
||||
fields,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
injectedMetadata: {
|
||||
getInjectedVar: jest.fn().mockImplementation(key => {
|
||||
switch (key) {
|
||||
case 'autocompleteTimeout':
|
||||
return 1000;
|
||||
case 'autocompleteTerminateAfter':
|
||||
return 100000;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}),
|
||||
},
|
||||
},
|
||||
data: {
|
||||
query: {
|
||||
filterManager: {
|
||||
fieldName: 'myField',
|
||||
getIndexPattern: () => ({
|
||||
fields,
|
||||
}),
|
||||
getAppFilters: jest.fn().mockImplementation(() => []),
|
||||
getGlobalFilters: jest.fn().mockImplementation(() => []),
|
||||
},
|
||||
timefilter: {
|
||||
timefilter: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any);
|
|
@ -17,7 +17,12 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const getIndexPatternMock = () => {
|
||||
import { IIndexPattern } from '../../../../../../../plugins/data/public';
|
||||
|
||||
/**
|
||||
* Returns forced **Partial** IndexPattern for use in tests
|
||||
*/
|
||||
export const getIndexPatternMock = (): Promise<IIndexPattern> => {
|
||||
return Promise.resolve({
|
||||
id: 'mockIndexPattern',
|
||||
title: 'mockIndexPattern',
|
||||
|
@ -26,5 +31,5 @@ export const getIndexPatternMock = () => {
|
|||
{ name: 'textField', type: 'string', aggregatable: false },
|
||||
{ name: 'numberField', type: 'number', aggregatable: true },
|
||||
],
|
||||
});
|
||||
} as IIndexPattern);
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SearchSource } from '../../../legacy_imports';
|
||||
|
||||
export const getSearchSourceMock = (esSearchResponse?: any): SearchSource =>
|
||||
jest.fn().mockImplementation(() => ({
|
||||
setParent: jest.fn(),
|
||||
setField: jest.fn(),
|
||||
fetch: jest.fn().mockResolvedValue(
|
||||
esSearchResponse
|
||||
? esSearchResponse
|
||||
: {
|
||||
aggregations: {
|
||||
termsAgg: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'Zurich Airport',
|
||||
doc_count: 691,
|
||||
},
|
||||
{
|
||||
key: 'Xi an Xianyang International Airport',
|
||||
doc_count: 526,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
}));
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { ShallowWrapper, ReactWrapper } from 'enzyme';
|
||||
|
||||
export const updateComponent = async (
|
||||
component:
|
||||
| ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>>
|
||||
| ReactWrapper<any, Readonly<{}>, React.Component<{}, {}, any>>
|
||||
) => {
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
};
|
|
@ -17,13 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { RangeControlEditor } from './range_control_editor';
|
||||
import { ListControlEditor } from './list_control_editor';
|
||||
import { getTitle } from '../../editor_utils';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { PureComponent, ChangeEvent } from 'react';
|
||||
import { InjectedIntlProps } from 'react-intl';
|
||||
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiButtonIcon,
|
||||
|
@ -32,11 +29,45 @@ import {
|
|||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiSwitchEvent,
|
||||
} from '@elastic/eui';
|
||||
|
||||
class ControlEditorUi extends Component {
|
||||
changeLabel = evt => {
|
||||
this.props.handleLabelChange(this.props.controlIndex, evt);
|
||||
import { RangeControlEditor } from './range_control_editor';
|
||||
import { ListControlEditor } from './list_control_editor';
|
||||
import { getTitle, ControlParams, CONTROL_TYPES, ControlParamsOptions } from '../../editor_utils';
|
||||
import { IIndexPattern } from '../../../../../../plugins/data/public';
|
||||
import { InputControlVisDependencies } from '../../plugin';
|
||||
|
||||
interface ControlEditorUiProps {
|
||||
controlIndex: number;
|
||||
controlParams: ControlParams;
|
||||
handleLabelChange: (controlIndex: number, event: ChangeEvent<HTMLInputElement>) => void;
|
||||
moveControl: (controlIndex: number, direction: number) => void;
|
||||
handleRemoveControl: (controlIndex: number) => void;
|
||||
handleIndexPatternChange: (controlIndex: number, indexPatternId: string) => void;
|
||||
handleFieldNameChange: (controlIndex: number, fieldName: string) => void;
|
||||
getIndexPattern: (indexPatternId: string) => Promise<IIndexPattern>;
|
||||
handleCheckboxOptionChange: (
|
||||
controlIndex: number,
|
||||
optionName: keyof ControlParamsOptions,
|
||||
event: EuiSwitchEvent
|
||||
) => void;
|
||||
handleNumberOptionChange: (
|
||||
controlIndex: number,
|
||||
optionName: keyof ControlParamsOptions,
|
||||
event: ChangeEvent<HTMLInputElement>
|
||||
) => void;
|
||||
parentCandidates: Array<{
|
||||
value: string;
|
||||
text: string;
|
||||
}>;
|
||||
handleParentChange: (controlIndex: number, event: ChangeEvent<HTMLSelectElement>) => void;
|
||||
deps: InputControlVisDependencies;
|
||||
}
|
||||
|
||||
class ControlEditorUi extends PureComponent<ControlEditorUiProps & InjectedIntlProps> {
|
||||
changeLabel = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.handleLabelChange(this.props.controlIndex, event);
|
||||
};
|
||||
|
||||
removeControl = () => {
|
||||
|
@ -51,18 +82,18 @@ class ControlEditorUi extends Component {
|
|||
this.props.moveControl(this.props.controlIndex, 1);
|
||||
};
|
||||
|
||||
changeIndexPattern = evt => {
|
||||
this.props.handleIndexPatternChange(this.props.controlIndex, evt);
|
||||
changeIndexPattern = (indexPatternId: string) => {
|
||||
this.props.handleIndexPatternChange(this.props.controlIndex, indexPatternId);
|
||||
};
|
||||
|
||||
changeFieldName = evt => {
|
||||
this.props.handleFieldNameChange(this.props.controlIndex, evt);
|
||||
changeFieldName = (fieldName: string) => {
|
||||
this.props.handleFieldNameChange(this.props.controlIndex, fieldName);
|
||||
};
|
||||
|
||||
renderEditor() {
|
||||
let controlEditor = null;
|
||||
switch (this.props.controlParams.type) {
|
||||
case 'list':
|
||||
case CONTROL_TYPES.LIST:
|
||||
controlEditor = (
|
||||
<ListControlEditor
|
||||
controlIndex={this.props.controlIndex}
|
||||
|
@ -74,10 +105,11 @@ class ControlEditorUi extends Component {
|
|||
handleCheckboxOptionChange={this.props.handleCheckboxOptionChange}
|
||||
parentCandidates={this.props.parentCandidates}
|
||||
handleParentChange={this.props.handleParentChange}
|
||||
deps={this.props.deps}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'range':
|
||||
case CONTROL_TYPES.RANGE:
|
||||
controlEditor = (
|
||||
<RangeControlEditor
|
||||
controlIndex={this.props.controlIndex}
|
||||
|
@ -86,6 +118,7 @@ class ControlEditorUi extends Component {
|
|||
handleFieldNameChange={this.changeFieldName}
|
||||
getIndexPattern={this.props.getIndexPattern}
|
||||
handleNumberOptionChange={this.props.handleNumberOptionChange}
|
||||
deps={this.props.deps}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -167,24 +200,4 @@ class ControlEditorUi extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
ControlEditorUi.propTypes = {
|
||||
controlIndex: PropTypes.number.isRequired,
|
||||
controlParams: PropTypes.object.isRequired,
|
||||
handleLabelChange: PropTypes.func.isRequired,
|
||||
moveControl: PropTypes.func.isRequired,
|
||||
handleRemoveControl: PropTypes.func.isRequired,
|
||||
handleIndexPatternChange: PropTypes.func.isRequired,
|
||||
handleFieldNameChange: PropTypes.func.isRequired,
|
||||
getIndexPattern: PropTypes.func.isRequired,
|
||||
handleCheckboxOptionChange: PropTypes.func.isRequired,
|
||||
handleNumberOptionChange: PropTypes.func.isRequired,
|
||||
parentCandidates: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
handleParentChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const ControlEditor = injectI18n(ControlEditorUi);
|
|
@ -17,45 +17,36 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
jest.mock('../../../../../core_plugins/data/public/legacy', () => ({
|
||||
indexPatterns: {
|
||||
indexPatterns: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('ui/new_platform', () => ({
|
||||
npStart: {
|
||||
plugins: {
|
||||
data: {
|
||||
ui: {
|
||||
IndexPatternSelect: () => {
|
||||
return <div />;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import React from 'react';
|
||||
import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
// @ts-ignore
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { getDepsMock } from './__tests__/get_deps_mock';
|
||||
import { getIndexPatternMock } from './__tests__/get_index_pattern_mock';
|
||||
import { ControlsTab } from './controls_tab';
|
||||
import { ControlsTab, ControlsTabUiProps } from './controls_tab';
|
||||
|
||||
const indexPatternsMock = {
|
||||
get: getIndexPatternMock,
|
||||
};
|
||||
let props;
|
||||
let props: ControlsTabUiProps;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
deps: getDepsMock(),
|
||||
vis: {
|
||||
API: {
|
||||
indexPatterns: indexPatternsMock,
|
||||
},
|
||||
type: {
|
||||
name: 'test',
|
||||
title: 'test',
|
||||
visualization: null,
|
||||
requestHandler: 'test',
|
||||
responseHandler: 'test',
|
||||
stage: 'beta',
|
||||
requiresSearch: false,
|
||||
hidden: false,
|
||||
},
|
||||
},
|
||||
stateParams: {
|
||||
controls: [
|
||||
|
@ -71,6 +62,7 @@ beforeEach(() => {
|
|||
size: 5,
|
||||
order: 'desc',
|
||||
},
|
||||
parent: 'parent',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
|
@ -81,10 +73,12 @@ beforeEach(() => {
|
|||
options: {
|
||||
step: 1,
|
||||
},
|
||||
parent: 'parent',
|
||||
},
|
||||
],
|
||||
},
|
||||
setValue: jest.fn(),
|
||||
intl: null as any,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -105,7 +99,7 @@ describe('behavior', () => {
|
|||
'controls',
|
||||
expect.arrayContaining(props.stateParams.controls)
|
||||
);
|
||||
expect(props.setValue.mock.calls[0][1].length).toEqual(3);
|
||||
expect((props.setValue as jest.Mock).mock.calls[0][1].length).toEqual(3);
|
||||
});
|
||||
|
||||
test('remove control button', () => {
|
||||
|
@ -120,6 +114,7 @@ describe('behavior', () => {
|
|||
fieldName: 'numberField',
|
||||
label: '',
|
||||
type: 'range',
|
||||
parent: 'parent',
|
||||
options: {
|
||||
step: 1,
|
||||
},
|
||||
|
@ -142,6 +137,7 @@ describe('behavior', () => {
|
|||
fieldName: 'numberField',
|
||||
label: '',
|
||||
type: 'range',
|
||||
parent: 'parent',
|
||||
options: {
|
||||
step: 1,
|
||||
},
|
||||
|
@ -152,6 +148,7 @@ describe('behavior', () => {
|
|||
fieldName: 'keywordField',
|
||||
label: 'custom label',
|
||||
type: 'list',
|
||||
parent: 'parent',
|
||||
options: {
|
||||
type: 'terms',
|
||||
multiselect: true,
|
||||
|
@ -177,6 +174,7 @@ describe('behavior', () => {
|
|||
fieldName: 'numberField',
|
||||
label: '',
|
||||
type: 'range',
|
||||
parent: 'parent',
|
||||
options: {
|
||||
step: 1,
|
||||
},
|
||||
|
@ -187,6 +185,7 @@ describe('behavior', () => {
|
|||
fieldName: 'keywordField',
|
||||
label: 'custom label',
|
||||
type: 'list',
|
||||
parent: 'parent',
|
||||
options: {
|
||||
type: 'terms',
|
||||
multiselect: true,
|
|
@ -17,14 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { ControlEditor } from './control_editor';
|
||||
import { addControl, moveControl, newControl, removeControl, setControl } from '../../editor_utils';
|
||||
import { getLineageMap, getParentCandidates } from '../../lineage';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import React, { PureComponent, ChangeEvent } from 'react';
|
||||
import { InjectedIntlProps } from 'react-intl';
|
||||
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
|
@ -32,55 +28,97 @@ import {
|
|||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiSelect,
|
||||
EuiSwitchEvent,
|
||||
} from '@elastic/eui';
|
||||
|
||||
class ControlsTabUi extends Component {
|
||||
import { ControlEditor } from './control_editor';
|
||||
import {
|
||||
addControl,
|
||||
moveControl,
|
||||
newControl,
|
||||
removeControl,
|
||||
setControl,
|
||||
ControlParams,
|
||||
CONTROL_TYPES,
|
||||
ControlParamsOptions,
|
||||
} from '../../editor_utils';
|
||||
import { getLineageMap, getParentCandidates } from '../../lineage';
|
||||
import { IIndexPattern } from '../../../../../../plugins/data/public';
|
||||
import { VisOptionsProps } from '../../legacy_imports';
|
||||
import { InputControlVisDependencies } from '../../plugin';
|
||||
|
||||
interface ControlsTabUiState {
|
||||
type: CONTROL_TYPES;
|
||||
}
|
||||
|
||||
interface ControlsTabUiParams {
|
||||
controls: ControlParams[];
|
||||
}
|
||||
type ControlsTabUiInjectedProps = InjectedIntlProps &
|
||||
Pick<VisOptionsProps<ControlsTabUiParams>, 'vis' | 'stateParams' | 'setValue'> & {
|
||||
deps: InputControlVisDependencies;
|
||||
};
|
||||
|
||||
export type ControlsTabUiProps = ControlsTabUiInjectedProps;
|
||||
|
||||
class ControlsTabUi extends PureComponent<ControlsTabUiProps, ControlsTabUiState> {
|
||||
state = {
|
||||
type: 'list',
|
||||
type: CONTROL_TYPES.LIST,
|
||||
};
|
||||
|
||||
getIndexPattern = async indexPatternId => {
|
||||
return await npStart.plugins.data.indexPatterns.get(indexPatternId);
|
||||
getIndexPattern = async (indexPatternId: string): Promise<IIndexPattern> => {
|
||||
const [, startDeps] = await this.props.deps.core.getStartServices();
|
||||
return await startDeps.data.indexPatterns.get(indexPatternId);
|
||||
};
|
||||
|
||||
onChange = value => this.props.setValue('controls', value);
|
||||
onChange = (value: ControlParams[]) => this.props.setValue('controls', value);
|
||||
|
||||
handleLabelChange = (controlIndex, evt) => {
|
||||
handleLabelChange = (controlIndex: number, event: ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedControl = this.props.stateParams.controls[controlIndex];
|
||||
updatedControl.label = evt.target.value;
|
||||
updatedControl.label = event.target.value;
|
||||
this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl));
|
||||
};
|
||||
|
||||
handleIndexPatternChange = (controlIndex, indexPatternId) => {
|
||||
handleIndexPatternChange = (controlIndex: number, indexPatternId: string) => {
|
||||
const updatedControl = this.props.stateParams.controls[controlIndex];
|
||||
updatedControl.indexPattern = indexPatternId;
|
||||
updatedControl.fieldName = '';
|
||||
this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl));
|
||||
};
|
||||
|
||||
handleFieldNameChange = (controlIndex, fieldName) => {
|
||||
handleFieldNameChange = (controlIndex: number, fieldName: string) => {
|
||||
const updatedControl = this.props.stateParams.controls[controlIndex];
|
||||
updatedControl.fieldName = fieldName;
|
||||
this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl));
|
||||
};
|
||||
|
||||
handleCheckboxOptionChange = (controlIndex, optionName, evt) => {
|
||||
handleCheckboxOptionChange = (
|
||||
controlIndex: number,
|
||||
optionName: keyof ControlParamsOptions,
|
||||
event: EuiSwitchEvent
|
||||
) => {
|
||||
const updatedControl = this.props.stateParams.controls[controlIndex];
|
||||
updatedControl.options[optionName] = evt.target.checked;
|
||||
// @ts-ignore
|
||||
updatedControl.options[optionName] = event.target.checked;
|
||||
this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl));
|
||||
};
|
||||
|
||||
handleNumberOptionChange = (controlIndex, optionName, evt) => {
|
||||
handleNumberOptionChange = (
|
||||
controlIndex: number,
|
||||
optionName: keyof ControlParamsOptions,
|
||||
event: ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const updatedControl = this.props.stateParams.controls[controlIndex];
|
||||
updatedControl.options[optionName] = parseFloat(evt.target.value);
|
||||
// @ts-ignore
|
||||
updatedControl.options[optionName] = parseFloat(event.target.value);
|
||||
this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl));
|
||||
};
|
||||
|
||||
handleRemoveControl = controlIndex => {
|
||||
handleRemoveControl = (controlIndex: number) => {
|
||||
this.onChange(removeControl(this.props.stateParams.controls, controlIndex));
|
||||
};
|
||||
|
||||
moveControl = (controlIndex, direction) => {
|
||||
moveControl = (controlIndex: number, direction: number) => {
|
||||
this.onChange(moveControl(this.props.stateParams.controls, controlIndex, direction));
|
||||
};
|
||||
|
||||
|
@ -88,9 +126,9 @@ class ControlsTabUi extends Component {
|
|||
this.onChange(addControl(this.props.stateParams.controls, newControl(this.state.type)));
|
||||
};
|
||||
|
||||
handleParentChange = (controlIndex, evt) => {
|
||||
handleParentChange = (controlIndex: number, event: ChangeEvent<HTMLSelectElement>) => {
|
||||
const updatedControl = this.props.stateParams.controls[controlIndex];
|
||||
updatedControl.parent = evt.target.value;
|
||||
updatedControl.parent = event.target.value;
|
||||
this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl));
|
||||
};
|
||||
|
||||
|
@ -117,6 +155,7 @@ class ControlsTabUi extends Component {
|
|||
handleNumberOptionChange={this.handleNumberOptionChange}
|
||||
parentCandidates={parentCandidates}
|
||||
handleParentChange={this.handleParentChange}
|
||||
deps={this.props.deps}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -137,14 +176,14 @@ class ControlsTabUi extends Component {
|
|||
data-test-subj="selectControlType"
|
||||
options={[
|
||||
{
|
||||
value: 'range',
|
||||
value: CONTROL_TYPES.RANGE,
|
||||
text: intl.formatMessage({
|
||||
id: 'inputControl.editor.controlsTab.select.rangeDropDownOptionLabel',
|
||||
defaultMessage: 'Range slider',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'list',
|
||||
value: CONTROL_TYPES.LIST,
|
||||
text: intl.formatMessage({
|
||||
id: 'inputControl.editor.controlsTab.select.listDropDownOptionLabel',
|
||||
defaultMessage: 'Options list',
|
||||
|
@ -152,7 +191,7 @@ class ControlsTabUi extends Component {
|
|||
},
|
||||
]}
|
||||
value={this.state.type}
|
||||
onChange={evt => this.setState({ type: evt.target.value })}
|
||||
onChange={event => this.setState({ type: event.target.value as CONTROL_TYPES })}
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'inputControl.editor.controlsTab.select.controlTypeAriaLabel',
|
||||
defaultMessage: 'Select control type',
|
||||
|
@ -186,9 +225,8 @@ class ControlsTabUi extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
ControlsTabUi.propTypes = {
|
||||
vis: PropTypes.object.isRequired,
|
||||
setValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const ControlsTab = injectI18n(ControlsTabUi);
|
||||
|
||||
export const getControlsTab = (deps: InputControlVisDependencies) => (
|
||||
props: Omit<ControlsTabUiProps, 'core'>
|
||||
) => <ControlsTab {...props} deps={deps} />;
|
|
@ -18,43 +18,59 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { InjectedIntlProps } from 'react-intl';
|
||||
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFormRow, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
|
||||
|
||||
import { EuiFormRow, EuiComboBox } from '@elastic/eui';
|
||||
import { IIndexPattern, IFieldType } from '../../../../../../plugins/data/public';
|
||||
|
||||
class FieldSelectUi extends Component {
|
||||
constructor(props) {
|
||||
interface FieldSelectUiState {
|
||||
isLoading: boolean;
|
||||
fields: Array<EuiComboBoxOptionProps<string>>;
|
||||
indexPatternId: string;
|
||||
}
|
||||
|
||||
export type FieldSelectUiProps = InjectedIntlProps & {
|
||||
getIndexPattern: (indexPatternId: string) => Promise<IIndexPattern>;
|
||||
indexPatternId: string;
|
||||
onChange: (value: any) => void;
|
||||
fieldName?: string;
|
||||
filterField?: (field: IFieldType) => boolean;
|
||||
controlIndex: number;
|
||||
};
|
||||
|
||||
class FieldSelectUi extends Component<FieldSelectUiProps, FieldSelectUiState> {
|
||||
private hasUnmounted: boolean;
|
||||
|
||||
constructor(props: FieldSelectUiProps) {
|
||||
super(props);
|
||||
|
||||
this._hasUnmounted = false;
|
||||
this.hasUnmounted = false;
|
||||
|
||||
this.state = {
|
||||
isLoading: false,
|
||||
fields: [],
|
||||
indexPatternId: props.indexPatternId,
|
||||
};
|
||||
this.filterField = _.get(props, 'filterField', () => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._hasUnmounted = true;
|
||||
this.hasUnmounted = true;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadFields(this.state.indexPatternId);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps: FieldSelectUiProps) {
|
||||
if (this.props.indexPatternId !== nextProps.indexPatternId) {
|
||||
this.loadFields(nextProps.indexPatternId);
|
||||
this.loadFields(nextProps.indexPatternId ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
loadFields = indexPatternId => {
|
||||
loadFields = (indexPatternId: string) => {
|
||||
this.setState(
|
||||
{
|
||||
isLoading: true,
|
||||
|
@ -65,12 +81,12 @@ class FieldSelectUi extends Component {
|
|||
);
|
||||
};
|
||||
|
||||
debouncedLoad = _.debounce(async indexPatternId => {
|
||||
debouncedLoad = _.debounce(async (indexPatternId: string) => {
|
||||
if (!indexPatternId || indexPatternId.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let indexPattern;
|
||||
let indexPattern: IIndexPattern;
|
||||
try {
|
||||
indexPattern = await this.props.getIndexPattern(indexPatternId);
|
||||
} catch (err) {
|
||||
|
@ -78,7 +94,7 @@ class FieldSelectUi extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this._hasUnmounted) {
|
||||
if (this.hasUnmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -88,17 +104,15 @@ class FieldSelectUi extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
const fieldsByTypeMap = new Map();
|
||||
const fields = [];
|
||||
indexPattern.fields.filter(this.filterField).forEach(field => {
|
||||
if (fieldsByTypeMap.has(field.type)) {
|
||||
const fieldsList = fieldsByTypeMap.get(field.type);
|
||||
const fieldsByTypeMap = new Map<string, string[]>();
|
||||
const fields: Array<EuiComboBoxOptionProps<string>> = [];
|
||||
indexPattern.fields
|
||||
.filter(this.props.filterField ?? (() => true))
|
||||
.forEach((field: IFieldType) => {
|
||||
const fieldsList = fieldsByTypeMap.get(field.type) ?? [];
|
||||
fieldsList.push(field.name);
|
||||
fieldsByTypeMap.set(field.type, fieldsList);
|
||||
} else {
|
||||
fieldsByTypeMap.set(field.type, [field.name]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
fieldsByTypeMap.forEach((fieldsList, fieldType) => {
|
||||
fields.push({
|
||||
|
@ -117,11 +131,11 @@ class FieldSelectUi extends Component {
|
|||
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
fields: fields,
|
||||
fields,
|
||||
});
|
||||
}, 300);
|
||||
|
||||
onChange = selectedOptions => {
|
||||
onChange = (selectedOptions: Array<EuiComboBoxOptionProps<any>>) => {
|
||||
this.props.onChange(_.get(selectedOptions, '0.value'));
|
||||
};
|
||||
|
||||
|
@ -165,13 +179,4 @@ class FieldSelectUi extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
FieldSelectUi.propTypes = {
|
||||
getIndexPattern: PropTypes.func.isRequired,
|
||||
indexPatternId: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
fieldName: PropTypes.string,
|
||||
filterField: PropTypes.func,
|
||||
controlIndex: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export const FieldSelect = injectI18n(FieldSelectUi);
|
|
@ -17,15 +17,20 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { injectI18n } from '@kbn/i18n/react';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { InjectedIntlProps } from 'react-intl';
|
||||
import { IndexPatternSelect } from 'src/plugins/data/public';
|
||||
|
||||
import { npStart } from 'ui/new_platform';
|
||||
const { IndexPatternSelect } = npStart.plugins.data.ui;
|
||||
export type IndexPatternSelectFormRowUiProps = InjectedIntlProps & {
|
||||
onChange: (opt: any) => void;
|
||||
indexPatternId: string;
|
||||
controlIndex: number;
|
||||
IndexPatternSelect: ComponentType<IndexPatternSelect['props']>;
|
||||
};
|
||||
|
||||
function IndexPatternSelectFormRowUi(props) {
|
||||
function IndexPatternSelectFormRowUi(props: IndexPatternSelectFormRowUiProps) {
|
||||
const { controlIndex, indexPatternId, intl, onChange } = props;
|
||||
const selectId = `indexPatternSelect-${controlIndex}`;
|
||||
|
||||
|
@ -37,7 +42,7 @@ function IndexPatternSelectFormRowUi(props) {
|
|||
defaultMessage: 'Index Pattern',
|
||||
})}
|
||||
>
|
||||
<IndexPatternSelect
|
||||
<props.IndexPatternSelect
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'inputControl.editor.indexPatternSelect.patternPlaceholder',
|
||||
defaultMessage: 'Select index pattern...',
|
||||
|
@ -45,15 +50,11 @@ function IndexPatternSelectFormRowUi(props) {
|
|||
indexPatternId={indexPatternId}
|
||||
onChange={onChange}
|
||||
data-test-subj={selectId}
|
||||
// TODO: supply actual savedObjectsClient here
|
||||
savedObjectsClient={{} as any}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
IndexPatternSelectFormRowUi.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
indexPatternId: PropTypes.string,
|
||||
controlIndex: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export const IndexPatternSelectFormRow = injectI18n(IndexPatternSelectFormRowUi);
|
|
@ -17,31 +17,21 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
jest.mock('ui/new_platform', () => ({
|
||||
npStart: {
|
||||
plugins: {
|
||||
data: {
|
||||
ui: {
|
||||
IndexPatternSelect: () => {
|
||||
return <div />;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
// @ts-ignore
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { getDepsMock } from './__tests__/get_deps_mock';
|
||||
import { getIndexPatternMock } from './__tests__/get_index_pattern_mock';
|
||||
|
||||
import { ListControlEditor } from './list_control_editor';
|
||||
import { ControlParams } from '../../editor_utils';
|
||||
import { updateComponent } from './__tests__/update_component';
|
||||
|
||||
const controlParams = {
|
||||
const controlParamsBase: ControlParams = {
|
||||
id: '1',
|
||||
indexPattern: 'indexPattern1',
|
||||
fieldName: 'keywordField',
|
||||
|
@ -53,11 +43,13 @@ const controlParams = {
|
|||
dynamicOptions: false,
|
||||
size: 10,
|
||||
},
|
||||
parent: '',
|
||||
};
|
||||
let handleFieldNameChange;
|
||||
let handleIndexPatternChange;
|
||||
let handleCheckboxOptionChange;
|
||||
let handleNumberOptionChange;
|
||||
const deps = getDepsMock();
|
||||
let handleFieldNameChange: sinon.SinonSpy;
|
||||
let handleIndexPatternChange: sinon.SinonSpy;
|
||||
let handleCheckboxOptionChange: sinon.SinonSpy;
|
||||
let handleNumberOptionChange: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
handleFieldNameChange = sinon.spy();
|
||||
|
@ -68,8 +60,9 @@ beforeEach(() => {
|
|||
|
||||
describe('renders', () => {
|
||||
test('should not display any options until field is selected', async () => {
|
||||
const controlParams = {
|
||||
const controlParams: ControlParams = {
|
||||
id: '1',
|
||||
label: 'mock',
|
||||
indexPattern: 'mockIndexPattern',
|
||||
fieldName: '',
|
||||
type: 'list',
|
||||
|
@ -79,9 +72,11 @@ describe('renders', () => {
|
|||
dynamicOptions: true,
|
||||
size: 5,
|
||||
},
|
||||
parent: '',
|
||||
};
|
||||
const component = shallow(
|
||||
<ListControlEditor
|
||||
deps={deps}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
|
@ -94,10 +89,7 @@ describe('renders', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
await updateComponent(component);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
@ -109,9 +101,10 @@ describe('renders', () => {
|
|||
];
|
||||
const component = shallow(
|
||||
<ListControlEditor
|
||||
deps={deps}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
controlParams={controlParamsBase}
|
||||
handleFieldNameChange={handleFieldNameChange}
|
||||
handleIndexPatternChange={handleIndexPatternChange}
|
||||
handleCheckboxOptionChange={handleCheckboxOptionChange}
|
||||
|
@ -121,18 +114,16 @@ describe('renders', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
await updateComponent(component);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('dynamic options', () => {
|
||||
test('should display dynamic options for string fields', async () => {
|
||||
const controlParams = {
|
||||
const controlParams: ControlParams = {
|
||||
id: '1',
|
||||
label: 'mock',
|
||||
indexPattern: 'mockIndexPattern',
|
||||
fieldName: 'keywordField',
|
||||
type: 'list',
|
||||
|
@ -142,9 +133,11 @@ describe('renders', () => {
|
|||
dynamicOptions: true,
|
||||
size: 5,
|
||||
},
|
||||
parent: '',
|
||||
};
|
||||
const component = shallow(
|
||||
<ListControlEditor
|
||||
deps={deps}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
|
@ -157,17 +150,15 @@ describe('renders', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
await updateComponent(component);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should display size field when dynamic options is disabled', async () => {
|
||||
const controlParams = {
|
||||
const controlParams: ControlParams = {
|
||||
id: '1',
|
||||
label: 'mock',
|
||||
indexPattern: 'mockIndexPattern',
|
||||
fieldName: 'keywordField',
|
||||
type: 'list',
|
||||
|
@ -177,9 +168,11 @@ describe('renders', () => {
|
|||
dynamicOptions: false,
|
||||
size: 5,
|
||||
},
|
||||
parent: '',
|
||||
};
|
||||
const component = shallow(
|
||||
<ListControlEditor
|
||||
deps={deps}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
|
@ -192,17 +185,15 @@ describe('renders', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
await updateComponent(component);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should display disabled dynamic options with tooltip for non-string fields', async () => {
|
||||
const controlParams = {
|
||||
const controlParams: ControlParams = {
|
||||
id: '1',
|
||||
label: 'mock',
|
||||
indexPattern: 'mockIndexPattern',
|
||||
fieldName: 'numberField',
|
||||
type: 'list',
|
||||
|
@ -212,9 +203,11 @@ describe('renders', () => {
|
|||
dynamicOptions: true,
|
||||
size: 5,
|
||||
},
|
||||
parent: '',
|
||||
};
|
||||
const component = shallow(
|
||||
<ListControlEditor
|
||||
deps={deps}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
|
@ -227,10 +220,7 @@ describe('renders', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
await updateComponent(component);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
@ -240,9 +230,10 @@ describe('renders', () => {
|
|||
test('handleCheckboxOptionChange - multiselect', async () => {
|
||||
const component = mountWithIntl(
|
||||
<ListControlEditor
|
||||
deps={deps}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
controlParams={controlParamsBase}
|
||||
handleFieldNameChange={handleFieldNameChange}
|
||||
handleIndexPatternChange={handleIndexPatternChange}
|
||||
handleCheckboxOptionChange={handleCheckboxOptionChange}
|
||||
|
@ -252,10 +243,7 @@ test('handleCheckboxOptionChange - multiselect', async () => {
|
|||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
await updateComponent(component);
|
||||
|
||||
const checkbox = findTestSubject(component, 'listControlMultiselectInput');
|
||||
checkbox.simulate('click');
|
||||
|
@ -268,10 +256,10 @@ test('handleCheckboxOptionChange - multiselect', async () => {
|
|||
handleCheckboxOptionChange,
|
||||
expectedControlIndex,
|
||||
expectedOptionName,
|
||||
sinon.match(evt => {
|
||||
// Synthetic `evt.target.checked` does not get altered by EuiSwitch,
|
||||
sinon.match(event => {
|
||||
// Synthetic `event.target.checked` does not get altered by EuiSwitch,
|
||||
// but its aria attribute is correctly updated
|
||||
if (evt.target.getAttribute('aria-checked') === 'true') {
|
||||
if (event.target.getAttribute('aria-checked') === 'true') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -282,9 +270,10 @@ test('handleCheckboxOptionChange - multiselect', async () => {
|
|||
test('handleNumberOptionChange - size', async () => {
|
||||
const component = mountWithIntl(
|
||||
<ListControlEditor
|
||||
deps={deps}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
controlParams={controlParamsBase}
|
||||
handleFieldNameChange={handleFieldNameChange}
|
||||
handleIndexPatternChange={handleIndexPatternChange}
|
||||
handleCheckboxOptionChange={handleCheckboxOptionChange}
|
||||
|
@ -294,10 +283,7 @@ test('handleNumberOptionChange - size', async () => {
|
|||
/>
|
||||
);
|
||||
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
await updateComponent(component);
|
||||
|
||||
const input = findTestSubject(component, 'listControlSizeInput');
|
||||
input.simulate('change', { target: { value: 7 } });
|
||||
|
@ -310,8 +296,8 @@ test('handleNumberOptionChange - size', async () => {
|
|||
handleNumberOptionChange,
|
||||
expectedControlIndex,
|
||||
expectedOptionName,
|
||||
sinon.match(evt => {
|
||||
if (evt.target.value === 7) {
|
||||
sinon.match(event => {
|
||||
if (event.target.value === 7) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -322,9 +308,10 @@ test('handleNumberOptionChange - size', async () => {
|
|||
test('field name change', async () => {
|
||||
const component = shallowWithIntl(
|
||||
<ListControlEditor
|
||||
deps={deps}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
controlParams={controlParamsBase}
|
||||
handleFieldNameChange={handleFieldNameChange}
|
||||
handleIndexPatternChange={handleIndexPatternChange}
|
||||
handleCheckboxOptionChange={handleCheckboxOptionChange}
|
||||
|
@ -334,25 +321,18 @@ test('field name change', async () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const update = async () => {
|
||||
// Ensure all promises resolve
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
};
|
||||
|
||||
// ensure that after async loading is complete the DynamicOptionsSwitch is not disabled
|
||||
expect(
|
||||
component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=false]')
|
||||
).toHaveLength(0);
|
||||
await update();
|
||||
await updateComponent(component);
|
||||
expect(
|
||||
component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=false]')
|
||||
).toHaveLength(1);
|
||||
|
||||
component.setProps({
|
||||
controlParams: {
|
||||
...controlParams,
|
||||
...controlParamsBase,
|
||||
fieldName: 'numberField',
|
||||
},
|
||||
});
|
||||
|
@ -361,20 +341,20 @@ test('field name change', async () => {
|
|||
expect(
|
||||
component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=true]')
|
||||
).toHaveLength(0);
|
||||
await update();
|
||||
await updateComponent(component);
|
||||
expect(
|
||||
component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=true]')
|
||||
).toHaveLength(1);
|
||||
|
||||
component.setProps({
|
||||
controlParams,
|
||||
controlParams: controlParamsBase,
|
||||
});
|
||||
|
||||
// ensure that after async loading is complete the DynamicOptionsSwitch is not disabled again, because we switched to original "string" field
|
||||
expect(
|
||||
component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=false]')
|
||||
).toHaveLength(0);
|
||||
await update();
|
||||
await updateComponent(component);
|
||||
expect(
|
||||
component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=false]')
|
||||
).toHaveLength(1);
|
|
@ -17,35 +17,90 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { PureComponent, ChangeEvent, ComponentType } from 'react';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiFieldNumber,
|
||||
EuiSwitch,
|
||||
EuiSelect,
|
||||
EuiSelectProps,
|
||||
EuiSwitchEvent,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { IndexPatternSelectFormRow } from './index_pattern_select_form_row';
|
||||
import { FieldSelect } from './field_select';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { ControlParams, ControlParamsOptions } from '../../editor_utils';
|
||||
import {
|
||||
IIndexPattern,
|
||||
IFieldType,
|
||||
IndexPatternSelect,
|
||||
} from '../../../../../../plugins/data/public';
|
||||
import { InputControlVisDependencies } from '../../plugin';
|
||||
|
||||
import { EuiFormRow, EuiFieldNumber, EuiSwitch, EuiSelect } from '@elastic/eui';
|
||||
|
||||
function filterField(field) {
|
||||
return field.aggregatable && ['number', 'boolean', 'date', 'ip', 'string'].includes(field.type);
|
||||
interface ListControlEditorState {
|
||||
isLoadingFieldType: boolean;
|
||||
isStringField: boolean;
|
||||
prevFieldName: string;
|
||||
IndexPatternSelect: ComponentType<IndexPatternSelect['props']> | null;
|
||||
}
|
||||
|
||||
export class ListControlEditor extends Component {
|
||||
state = {
|
||||
interface ListControlEditorProps {
|
||||
getIndexPattern: (indexPatternId: string) => Promise<IIndexPattern>;
|
||||
controlIndex: number;
|
||||
controlParams: ControlParams;
|
||||
handleFieldNameChange: (fieldName: string) => void;
|
||||
handleIndexPatternChange: (indexPatternId: string) => void;
|
||||
handleCheckboxOptionChange: (
|
||||
controlIndex: number,
|
||||
optionName: keyof ControlParamsOptions,
|
||||
event: EuiSwitchEvent
|
||||
) => void;
|
||||
handleNumberOptionChange: (
|
||||
controlIndex: number,
|
||||
optionName: keyof ControlParamsOptions,
|
||||
event: ChangeEvent<HTMLInputElement>
|
||||
) => void;
|
||||
handleParentChange: (controlIndex: number, event: ChangeEvent<HTMLSelectElement>) => void;
|
||||
parentCandidates: EuiSelectProps['options'];
|
||||
deps: InputControlVisDependencies;
|
||||
}
|
||||
|
||||
function filterField(field: IFieldType) {
|
||||
return (
|
||||
Boolean(field.aggregatable) &&
|
||||
['number', 'boolean', 'date', 'ip', 'string'].includes(field.type)
|
||||
);
|
||||
}
|
||||
|
||||
export class ListControlEditor extends PureComponent<
|
||||
ListControlEditorProps,
|
||||
ListControlEditorState
|
||||
> {
|
||||
private isMounted: boolean = false;
|
||||
|
||||
state: ListControlEditorState = {
|
||||
isLoadingFieldType: true,
|
||||
isStringField: false,
|
||||
prevFieldName: this.props.controlParams.fieldName,
|
||||
IndexPatternSelect: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
this.isMounted = true;
|
||||
this.loadIsStringField();
|
||||
this.getIndexPatternSelect();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps = (nextProps, prevState) => {
|
||||
static getDerivedStateFromProps = (
|
||||
nextProps: ListControlEditorProps,
|
||||
prevState: ListControlEditorState
|
||||
) => {
|
||||
const isNewFieldName = prevState.prevFieldName !== nextProps.controlParams.fieldName;
|
||||
if (!prevState.isLoadingFieldType && isNewFieldName) {
|
||||
return {
|
||||
|
@ -63,13 +118,20 @@ export class ListControlEditor extends Component {
|
|||
}
|
||||
};
|
||||
|
||||
async getIndexPatternSelect() {
|
||||
const [, { data }] = await this.props.deps.core.getStartServices();
|
||||
this.setState({
|
||||
IndexPatternSelect: data.ui.IndexPatternSelect,
|
||||
});
|
||||
}
|
||||
|
||||
loadIsStringField = async () => {
|
||||
if (!this.props.controlParams.indexPattern || !this.props.controlParams.fieldName) {
|
||||
this.setState({ isLoadingFieldType: false });
|
||||
return;
|
||||
}
|
||||
|
||||
let indexPattern;
|
||||
let indexPattern: IIndexPattern;
|
||||
try {
|
||||
indexPattern = await this.props.getIndexPattern(this.props.controlParams.indexPattern);
|
||||
} catch (err) {
|
||||
|
@ -77,13 +139,13 @@ export class ListControlEditor extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!this._isMounted) {
|
||||
if (!this.isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const field = indexPattern.fields.find(field => {
|
||||
return field.name === this.props.controlParams.fieldName;
|
||||
});
|
||||
const field = (indexPattern.fields as IFieldType[]).find(
|
||||
({ name }) => name === this.props.controlParams.fieldName
|
||||
);
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
@ -121,8 +183,8 @@ export class ListControlEditor extends Component {
|
|||
<EuiSelect
|
||||
options={parentCandidatesOptions}
|
||||
value={this.props.controlParams.parent}
|
||||
onChange={evt => {
|
||||
this.props.handleParentChange(this.props.controlIndex, evt);
|
||||
onChange={event => {
|
||||
this.props.handleParentChange(this.props.controlIndex, event);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
@ -147,9 +209,9 @@ export class ListControlEditor extends Component {
|
|||
defaultMessage="Multiselect"
|
||||
/>
|
||||
}
|
||||
checked={this.props.controlParams.options.multiselect}
|
||||
onChange={evt => {
|
||||
this.props.handleCheckboxOptionChange(this.props.controlIndex, 'multiselect', evt);
|
||||
checked={this.props.controlParams.options.multiselect ?? true}
|
||||
onChange={event => {
|
||||
this.props.handleCheckboxOptionChange(this.props.controlIndex, 'multiselect', event);
|
||||
}}
|
||||
data-test-subj="listControlMultiselectInput"
|
||||
/>
|
||||
|
@ -180,9 +242,9 @@ export class ListControlEditor extends Component {
|
|||
defaultMessage="Dynamic Options"
|
||||
/>
|
||||
}
|
||||
checked={this.props.controlParams.options.dynamicOptions}
|
||||
onChange={evt => {
|
||||
this.props.handleCheckboxOptionChange(this.props.controlIndex, 'dynamicOptions', evt);
|
||||
checked={this.props.controlParams.options.dynamicOptions ?? false}
|
||||
onChange={event => {
|
||||
this.props.handleCheckboxOptionChange(this.props.controlIndex, 'dynamicOptions', event);
|
||||
}}
|
||||
disabled={this.state.isStringField ? false : true}
|
||||
data-test-subj="listControlDynamicOptionsSwitch"
|
||||
|
@ -212,8 +274,8 @@ export class ListControlEditor extends Component {
|
|||
<EuiFieldNumber
|
||||
min={1}
|
||||
value={this.props.controlParams.options.size}
|
||||
onChange={evt => {
|
||||
this.props.handleNumberOptionChange(this.props.controlIndex, 'size', evt);
|
||||
onChange={event => {
|
||||
this.props.handleNumberOptionChange(this.props.controlIndex, 'size', event);
|
||||
}}
|
||||
data-test-subj="listControlSizeInput"
|
||||
/>
|
||||
|
@ -225,12 +287,17 @@ export class ListControlEditor extends Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
if (this.state.IndexPatternSelect === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<>
|
||||
<IndexPatternSelectFormRow
|
||||
indexPatternId={this.props.controlParams.indexPattern}
|
||||
onChange={this.props.handleIndexPatternChange}
|
||||
controlIndex={this.props.controlIndex}
|
||||
IndexPatternSelect={this.state.IndexPatternSelect}
|
||||
/>
|
||||
|
||||
<FieldSelect
|
||||
|
@ -243,24 +310,7 @@ export class ListControlEditor extends Component {
|
|||
/>
|
||||
|
||||
{this.renderOptions()}
|
||||
</Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ListControlEditor.propTypes = {
|
||||
getIndexPattern: PropTypes.func.isRequired,
|
||||
controlIndex: PropTypes.number.isRequired,
|
||||
controlParams: PropTypes.object.isRequired,
|
||||
handleFieldNameChange: PropTypes.func.isRequired,
|
||||
handleIndexPatternChange: PropTypes.func.isRequired,
|
||||
handleCheckboxOptionChange: PropTypes.func.isRequired,
|
||||
handleNumberOptionChange: PropTypes.func.isRequired,
|
||||
parentCandidates: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
value: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
handleParentChange: PropTypes.func.isRequired,
|
||||
};
|
|
@ -21,17 +21,19 @@ import React from 'react';
|
|||
import { shallow } from 'enzyme';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
import { OptionsTab } from './options_tab';
|
||||
import { OptionsTab, OptionsTabProps } from './options_tab';
|
||||
import { Vis } from '../../legacy_imports';
|
||||
|
||||
describe('OptionsTab', () => {
|
||||
let props;
|
||||
let props: OptionsTabProps;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
vis: {},
|
||||
vis: {} as Vis,
|
||||
stateParams: {
|
||||
updateFiltersOnChange: false,
|
||||
useTimeFilter: false,
|
||||
pinFilters: false,
|
||||
},
|
||||
setValue: jest.fn(),
|
||||
};
|
|
@ -17,24 +17,37 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { EuiForm, EuiFormRow, EuiSwitch } from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiSwitchEvent } from '@elastic/eui';
|
||||
|
||||
export class OptionsTab extends Component {
|
||||
handleUpdateFiltersChange = evt => {
|
||||
this.props.setValue('updateFiltersOnChange', evt.target.checked);
|
||||
import { VisOptionsProps } from '../../legacy_imports';
|
||||
|
||||
interface OptionsTabParams {
|
||||
updateFiltersOnChange: boolean;
|
||||
useTimeFilter: boolean;
|
||||
pinFilters: boolean;
|
||||
}
|
||||
type OptionsTabInjectedProps = Pick<
|
||||
VisOptionsProps<OptionsTabParams>,
|
||||
'vis' | 'setValue' | 'stateParams'
|
||||
>;
|
||||
|
||||
export type OptionsTabProps = OptionsTabInjectedProps;
|
||||
|
||||
export class OptionsTab extends PureComponent<OptionsTabProps> {
|
||||
handleUpdateFiltersChange = (event: EuiSwitchEvent) => {
|
||||
this.props.setValue('updateFiltersOnChange', event.target.checked);
|
||||
};
|
||||
|
||||
handleUseTimeFilter = evt => {
|
||||
this.props.setValue('useTimeFilter', evt.target.checked);
|
||||
handleUseTimeFilter = (event: EuiSwitchEvent) => {
|
||||
this.props.setValue('useTimeFilter', event.target.checked);
|
||||
};
|
||||
|
||||
handlePinFilters = evt => {
|
||||
this.props.setValue('pinFilters', evt.target.checked);
|
||||
handlePinFilters = (event: EuiSwitchEvent) => {
|
||||
this.props.setValue('pinFilters', event.target.checked);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -85,8 +98,3 @@ export class OptionsTab extends Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
OptionsTab.propTypes = {
|
||||
vis: PropTypes.object.isRequired,
|
||||
setValue: PropTypes.func.isRequired,
|
||||
};
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment } from 'react';
|
||||
import { IndexPatternSelectFormRow } from './index_pattern_select_form_row';
|
||||
import { FieldSelect } from './field_select';
|
||||
|
||||
import { EuiFormRow, EuiFieldNumber } from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
function filterField(field) {
|
||||
return field.type === 'number';
|
||||
}
|
||||
|
||||
export function RangeControlEditor(props) {
|
||||
const stepSizeId = `stepSize-${props.controlIndex}`;
|
||||
const decimalPlacesId = `decimalPlaces-${props.controlIndex}`;
|
||||
const handleDecimalPlacesChange = evt => {
|
||||
props.handleNumberOptionChange(props.controlIndex, 'decimalPlaces', evt);
|
||||
};
|
||||
const handleStepChange = evt => {
|
||||
props.handleNumberOptionChange(props.controlIndex, 'step', evt);
|
||||
};
|
||||
return (
|
||||
<Fragment>
|
||||
<IndexPatternSelectFormRow
|
||||
indexPatternId={props.controlParams.indexPattern}
|
||||
onChange={props.handleIndexPatternChange}
|
||||
controlIndex={props.controlIndex}
|
||||
/>
|
||||
|
||||
<FieldSelect
|
||||
fieldName={props.controlParams.fieldName}
|
||||
indexPatternId={props.controlParams.indexPattern}
|
||||
filterField={filterField}
|
||||
onChange={props.handleFieldNameChange}
|
||||
getIndexPattern={props.getIndexPattern}
|
||||
controlIndex={props.controlIndex}
|
||||
/>
|
||||
|
||||
<EuiFormRow
|
||||
id={stepSizeId}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="inputControl.editor.rangeControl.stepSizeLabel"
|
||||
defaultMessage="Step Size"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
value={props.controlParams.options.step}
|
||||
onChange={handleStepChange}
|
||||
data-test-subj={`rangeControlSizeInput${props.controlIndex}`}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
id={decimalPlacesId}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="inputControl.editor.rangeControl.decimalPlacesLabel"
|
||||
defaultMessage="Decimal Places"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
min={0}
|
||||
value={props.controlParams.options.decimalPlaces}
|
||||
onChange={handleDecimalPlacesChange}
|
||||
data-test-subj={`rangeControlDecimalPlacesInput${props.controlIndex}`}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
RangeControlEditor.propTypes = {
|
||||
getIndexPattern: PropTypes.func.isRequired,
|
||||
controlIndex: PropTypes.number.isRequired,
|
||||
controlParams: PropTypes.object.isRequired,
|
||||
handleFieldNameChange: PropTypes.func.isRequired,
|
||||
handleIndexPatternChange: PropTypes.func.isRequired,
|
||||
handleNumberOptionChange: PropTypes.func.isRequired,
|
||||
};
|
|
@ -18,30 +18,20 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sinon from 'sinon';
|
||||
import { shallow } from 'enzyme';
|
||||
import { SinonSpy, spy, assert, match } from 'sinon';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
jest.mock('ui/new_platform', () => ({
|
||||
npStart: {
|
||||
plugins: {
|
||||
data: {
|
||||
ui: {
|
||||
IndexPatternSelect: () => {
|
||||
return <div />;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// @ts-ignore
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { getIndexPatternMock } from './__tests__/get_index_pattern_mock';
|
||||
|
||||
import { RangeControlEditor } from './range_control_editor';
|
||||
import { ControlParams } from '../../editor_utils';
|
||||
import { getDepsMock } from './__tests__/get_deps_mock';
|
||||
import { updateComponent } from './__tests__/update_component';
|
||||
|
||||
const controlParams = {
|
||||
const controlParams: ControlParams = {
|
||||
id: '1',
|
||||
indexPattern: 'indexPattern1',
|
||||
fieldName: 'numberField',
|
||||
|
@ -51,20 +41,23 @@ const controlParams = {
|
|||
decimalPlaces: 0,
|
||||
step: 1,
|
||||
},
|
||||
parent: '',
|
||||
};
|
||||
let handleFieldNameChange;
|
||||
let handleIndexPatternChange;
|
||||
let handleNumberOptionChange;
|
||||
const deps = getDepsMock();
|
||||
let handleFieldNameChange: SinonSpy;
|
||||
let handleIndexPatternChange: SinonSpy;
|
||||
let handleNumberOptionChange: SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
handleFieldNameChange = sinon.spy();
|
||||
handleIndexPatternChange = sinon.spy();
|
||||
handleNumberOptionChange = sinon.spy();
|
||||
handleFieldNameChange = spy();
|
||||
handleIndexPatternChange = spy();
|
||||
handleNumberOptionChange = spy();
|
||||
});
|
||||
|
||||
test('renders RangeControlEditor', () => {
|
||||
test('renders RangeControlEditor', async () => {
|
||||
const component = shallow(
|
||||
<RangeControlEditor
|
||||
deps={deps}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
|
@ -73,12 +66,16 @@ test('renders RangeControlEditor', () => {
|
|||
handleNumberOptionChange={handleNumberOptionChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await updateComponent(component);
|
||||
|
||||
expect(component).toMatchSnapshot(); // eslint-disable-line
|
||||
});
|
||||
|
||||
test('handleNumberOptionChange - step', () => {
|
||||
test('handleNumberOptionChange - step', async () => {
|
||||
const component = mountWithIntl(
|
||||
<RangeControlEditor
|
||||
deps={deps}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
|
@ -87,19 +84,22 @@ test('handleNumberOptionChange - step', () => {
|
|||
handleNumberOptionChange={handleNumberOptionChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await updateComponent(component);
|
||||
|
||||
findTestSubject(component, 'rangeControlSizeInput0').simulate('change', {
|
||||
target: { value: 0.5 },
|
||||
});
|
||||
sinon.assert.notCalled(handleFieldNameChange);
|
||||
sinon.assert.notCalled(handleIndexPatternChange);
|
||||
assert.notCalled(handleFieldNameChange);
|
||||
assert.notCalled(handleIndexPatternChange);
|
||||
const expectedControlIndex = 0;
|
||||
const expectedOptionName = 'step';
|
||||
sinon.assert.calledWith(
|
||||
assert.calledWith(
|
||||
handleNumberOptionChange,
|
||||
expectedControlIndex,
|
||||
expectedOptionName,
|
||||
sinon.match(evt => {
|
||||
if (evt.target.value === 0.5) {
|
||||
match(event => {
|
||||
if (event.target.value === 0.5) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -107,9 +107,10 @@ test('handleNumberOptionChange - step', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('handleNumberOptionChange - decimalPlaces', () => {
|
||||
test('handleNumberOptionChange - decimalPlaces', async () => {
|
||||
const component = mountWithIntl(
|
||||
<RangeControlEditor
|
||||
deps={deps}
|
||||
getIndexPattern={getIndexPatternMock}
|
||||
controlIndex={0}
|
||||
controlParams={controlParams}
|
||||
|
@ -118,19 +119,22 @@ test('handleNumberOptionChange - decimalPlaces', () => {
|
|||
handleNumberOptionChange={handleNumberOptionChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await updateComponent(component);
|
||||
|
||||
findTestSubject(component, 'rangeControlDecimalPlacesInput0').simulate('change', {
|
||||
target: { value: 2 },
|
||||
});
|
||||
sinon.assert.notCalled(handleFieldNameChange);
|
||||
sinon.assert.notCalled(handleIndexPatternChange);
|
||||
assert.notCalled(handleFieldNameChange);
|
||||
assert.notCalled(handleIndexPatternChange);
|
||||
const expectedControlIndex = 0;
|
||||
const expectedOptionName = 'decimalPlaces';
|
||||
sinon.assert.calledWith(
|
||||
assert.calledWith(
|
||||
handleNumberOptionChange,
|
||||
expectedControlIndex,
|
||||
expectedOptionName,
|
||||
sinon.match(evt => {
|
||||
if (evt.target.value === 2) {
|
||||
match(event => {
|
||||
if (event.target.value === 2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { Component, Fragment, ChangeEvent, ComponentType } from 'react';
|
||||
|
||||
import { EuiFormRow, EuiFieldNumber } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { IndexPatternSelectFormRow } from './index_pattern_select_form_row';
|
||||
import { FieldSelect } from './field_select';
|
||||
import { ControlParams, ControlParamsOptions } from '../../editor_utils';
|
||||
import {
|
||||
IIndexPattern,
|
||||
IFieldType,
|
||||
IndexPatternSelect,
|
||||
} from '../../../../../../plugins/data/public';
|
||||
import { InputControlVisDependencies } from '../../plugin';
|
||||
|
||||
interface RangeControlEditorProps {
|
||||
controlIndex: number;
|
||||
controlParams: ControlParams;
|
||||
getIndexPattern: (indexPatternId: string) => Promise<IIndexPattern>;
|
||||
handleFieldNameChange: (fieldName: string) => void;
|
||||
handleIndexPatternChange: (indexPatternId: string) => void;
|
||||
handleNumberOptionChange: (
|
||||
controlIndex: number,
|
||||
optionName: keyof ControlParamsOptions,
|
||||
event: ChangeEvent<HTMLInputElement>
|
||||
) => void;
|
||||
deps: InputControlVisDependencies;
|
||||
}
|
||||
|
||||
interface RangeControlEditorState {
|
||||
IndexPatternSelect: ComponentType<IndexPatternSelect['props']> | null;
|
||||
}
|
||||
|
||||
function filterField(field: IFieldType) {
|
||||
return field.type === 'number';
|
||||
}
|
||||
|
||||
export class RangeControlEditor extends Component<
|
||||
RangeControlEditorProps,
|
||||
RangeControlEditorState
|
||||
> {
|
||||
state: RangeControlEditorState = {
|
||||
IndexPatternSelect: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.getIndexPatternSelect();
|
||||
}
|
||||
|
||||
async getIndexPatternSelect() {
|
||||
const [, { data }] = await this.props.deps.core.getStartServices();
|
||||
this.setState({
|
||||
IndexPatternSelect: data.ui.IndexPatternSelect,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const stepSizeId = `stepSize-${this.props.controlIndex}`;
|
||||
const decimalPlacesId = `decimalPlaces-${this.props.controlIndex}`;
|
||||
if (this.state.IndexPatternSelect === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<IndexPatternSelectFormRow
|
||||
indexPatternId={this.props.controlParams.indexPattern}
|
||||
onChange={this.props.handleIndexPatternChange}
|
||||
controlIndex={this.props.controlIndex}
|
||||
IndexPatternSelect={this.state.IndexPatternSelect}
|
||||
/>
|
||||
|
||||
<FieldSelect
|
||||
fieldName={this.props.controlParams.fieldName}
|
||||
indexPatternId={this.props.controlParams.indexPattern}
|
||||
filterField={filterField}
|
||||
onChange={this.props.handleFieldNameChange}
|
||||
getIndexPattern={this.props.getIndexPattern}
|
||||
controlIndex={this.props.controlIndex}
|
||||
/>
|
||||
|
||||
<EuiFormRow
|
||||
id={stepSizeId}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="inputControl.editor.rangeControl.stepSizeLabel"
|
||||
defaultMessage="Step Size"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
value={this.props.controlParams.options.step}
|
||||
onChange={event => {
|
||||
this.props.handleNumberOptionChange(this.props.controlIndex, 'step', event);
|
||||
}}
|
||||
data-test-subj={`rangeControlSizeInput${this.props.controlIndex}`}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
id={decimalPlacesId}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="inputControl.editor.rangeControl.decimalPlacesLabel"
|
||||
defaultMessage="Decimal Places"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
min={0}
|
||||
value={this.props.controlParams.options.decimalPlaces}
|
||||
onChange={event => {
|
||||
this.props.handleNumberOptionChange(this.props.controlIndex, 'decimalPlaces', event);
|
||||
}}
|
||||
data-test-subj={`rangeControlDecimalPlacesInput${this.props.controlIndex}`}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -47,7 +47,6 @@ exports[`renders disabled control with tooltip 1`] = `
|
|||
anchorClassName="eui-displayBlock"
|
||||
content="I am disabled for testing purposes"
|
||||
delay="regular"
|
||||
placement="top"
|
||||
position="top"
|
||||
>
|
||||
<div>
|
|
@ -18,7 +18,6 @@ exports[`Apply and Cancel change btns enabled when there are changes 1`] = `
|
|||
>
|
||||
<InjectIntl(ListControlUi)
|
||||
controlIndex={0}
|
||||
disableMsg={null}
|
||||
fetchOptions={[Function]}
|
||||
formatOptionLabel={[Function]}
|
||||
id="mock-list-control"
|
||||
|
@ -106,7 +105,6 @@ exports[`Clear btns enabled when there are values 1`] = `
|
|||
>
|
||||
<InjectIntl(ListControlUi)
|
||||
controlIndex={0}
|
||||
disableMsg={null}
|
||||
fetchOptions={[Function]}
|
||||
formatOptionLabel={[Function]}
|
||||
id="mock-list-control"
|
||||
|
@ -194,7 +192,6 @@ exports[`Renders list control 1`] = `
|
|||
>
|
||||
<InjectIntl(ListControlUi)
|
||||
controlIndex={0}
|
||||
disableMsg={null}
|
||||
fetchOptions={[Function]}
|
||||
formatOptionLabel={[Function]}
|
||||
id="mock-list-control"
|
|
@ -24,6 +24,7 @@ exports[`renders ListControl 1`] = `
|
|||
label="list control"
|
||||
>
|
||||
<EuiComboBox
|
||||
async={false}
|
||||
compressed={false}
|
||||
data-test-subj="listControlSelect0"
|
||||
fullWidth={false}
|
|
@ -20,7 +20,6 @@ exports[`disabled 1`] = `
|
|||
exports[`renders RangeControl 1`] = `
|
||||
<FormRow
|
||||
controlIndex={0}
|
||||
disableMsg={null}
|
||||
id="mock-range-control"
|
||||
label="range control"
|
||||
>
|
|
@ -17,16 +17,24 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
import { EuiFormRow, EuiToolTip, EuiIcon } from '@elastic/eui';
|
||||
|
||||
export function FormRow(props) {
|
||||
export interface FormRowProps {
|
||||
label: string;
|
||||
warningMsg?: string;
|
||||
id: string;
|
||||
children: ReactElement;
|
||||
controlIndex: number;
|
||||
disableMsg?: string;
|
||||
}
|
||||
|
||||
export function FormRow(props: FormRowProps) {
|
||||
let control = props.children;
|
||||
if (props.disableMsg) {
|
||||
control = (
|
||||
<EuiToolTip placement="top" content={props.disableMsg} anchorClassName="eui-displayBlock">
|
||||
<EuiToolTip position="top" content={props.disableMsg} anchorClassName="eui-displayBlock">
|
||||
{control}
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
@ -49,12 +57,3 @@ export function FormRow(props) {
|
|||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
FormRow.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
warningMsg: PropTypes.string,
|
||||
id: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
controlIndex: PropTypes.number.isRequired,
|
||||
disableMsg: PropTypes.string,
|
||||
};
|
|
@ -21,11 +21,16 @@ import React from 'react';
|
|||
import sinon from 'sinon';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
// @ts-ignore
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
|
||||
import { InputControlVis } from './input_control_vis';
|
||||
import { ListControl } from '../../control/list_control_factory';
|
||||
import { RangeControl } from '../../control/range_control_factory';
|
||||
|
||||
const mockListControl = {
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
const mockListControl: ListControl = {
|
||||
id: 'mock-list-control',
|
||||
isEnabled: () => {
|
||||
return true;
|
||||
|
@ -38,11 +43,9 @@ const mockListControl = {
|
|||
label: 'list control',
|
||||
value: [],
|
||||
selectOptions: ['choice1', 'choice2'],
|
||||
format: value => {
|
||||
return value;
|
||||
},
|
||||
};
|
||||
const mockRangeControl = {
|
||||
format: (value: any) => value,
|
||||
} as ListControl;
|
||||
const mockRangeControl: RangeControl = {
|
||||
id: 'mock-range-control',
|
||||
isEnabled: () => {
|
||||
return true;
|
||||
|
@ -56,16 +59,16 @@ const mockRangeControl = {
|
|||
value: { min: 0, max: 0 },
|
||||
min: 0,
|
||||
max: 100,
|
||||
format: value => {
|
||||
return value;
|
||||
},
|
||||
};
|
||||
format: (value: any) => value,
|
||||
} as RangeControl;
|
||||
const updateFiltersOnChange = false;
|
||||
|
||||
let stageFilter;
|
||||
let submitFilters;
|
||||
let resetControls;
|
||||
let clearControls;
|
||||
const refreshControlMock = () => Promise.resolve();
|
||||
|
||||
let stageFilter: sinon.SinonSpy;
|
||||
let submitFilters: sinon.SinonSpy;
|
||||
let resetControls: sinon.SinonSpy;
|
||||
let clearControls: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
stageFilter = sinon.spy();
|
||||
|
@ -89,7 +92,7 @@ test('Renders list control', () => {
|
|||
hasValues={() => {
|
||||
return false;
|
||||
}}
|
||||
refreshControl={() => {}}
|
||||
refreshControl={refreshControlMock}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot(); // eslint-disable-line
|
||||
|
@ -110,7 +113,7 @@ test('Renders range control', () => {
|
|||
hasValues={() => {
|
||||
return false;
|
||||
}}
|
||||
refreshControl={() => {}}
|
||||
refreshControl={refreshControlMock}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot(); // eslint-disable-line
|
||||
|
@ -131,7 +134,7 @@ test('Apply and Cancel change btns enabled when there are changes', () => {
|
|||
hasValues={() => {
|
||||
return false;
|
||||
}}
|
||||
refreshControl={() => {}}
|
||||
refreshControl={refreshControlMock}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot(); // eslint-disable-line
|
||||
|
@ -152,7 +155,7 @@ test('Clear btns enabled when there are values', () => {
|
|||
hasValues={() => {
|
||||
return true;
|
||||
}}
|
||||
refreshControl={() => {}}
|
||||
refreshControl={refreshControlMock}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot(); // eslint-disable-line
|
||||
|
@ -173,7 +176,7 @@ test('clearControls', () => {
|
|||
hasValues={() => {
|
||||
return true;
|
||||
}}
|
||||
refreshControl={() => {}}
|
||||
refreshControl={refreshControlMock}
|
||||
/>
|
||||
);
|
||||
findTestSubject(component, 'inputControlClearBtn').simulate('click');
|
||||
|
@ -198,7 +201,7 @@ test('submitFilters', () => {
|
|||
hasValues={() => {
|
||||
return true;
|
||||
}}
|
||||
refreshControl={() => {}}
|
||||
refreshControl={refreshControlMock}
|
||||
/>
|
||||
);
|
||||
findTestSubject(component, 'inputControlSubmitBtn').simulate('click');
|
||||
|
@ -223,7 +226,7 @@ test('resetControls', () => {
|
|||
hasValues={() => {
|
||||
return true;
|
||||
}}
|
||||
refreshControl={() => {}}
|
||||
refreshControl={refreshControlMock}
|
||||
/>
|
||||
);
|
||||
findTestSubject(component, 'inputControlCancelBtn').simulate('click');
|
|
@ -17,16 +17,37 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { RangeControl } from './range_control';
|
||||
import { ListControl } from './list_control';
|
||||
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { CONTROL_TYPES } from '../../editor_utils';
|
||||
import { ListControl } from '../../control/list_control_factory';
|
||||
import { RangeControl } from '../../control/range_control_factory';
|
||||
import { ListControl as ListControlComponent } from '../vis/list_control';
|
||||
import { RangeControl as RangeControlComponent } from '../vis/range_control';
|
||||
|
||||
export class InputControlVis extends Component {
|
||||
constructor(props) {
|
||||
function isListControl(control: RangeControl | ListControl): control is ListControl {
|
||||
return control.type === CONTROL_TYPES.LIST;
|
||||
}
|
||||
|
||||
function isRangeControl(control: RangeControl | ListControl): control is RangeControl {
|
||||
return control.type === CONTROL_TYPES.RANGE;
|
||||
}
|
||||
|
||||
interface InputControlVisProps {
|
||||
stageFilter: (controlIndex: number, newValue: any) => void;
|
||||
submitFilters: () => void;
|
||||
resetControls: () => void;
|
||||
clearControls: () => void;
|
||||
controls: Array<RangeControl | ListControl>;
|
||||
updateFiltersOnChange?: boolean;
|
||||
hasChanges: () => boolean;
|
||||
hasValues: () => boolean;
|
||||
refreshControl: (controlIndex: number, query: any) => Promise<void>;
|
||||
}
|
||||
|
||||
export class InputControlVis extends Component<InputControlVisProps> {
|
||||
constructor(props: InputControlVisProps) {
|
||||
super(props);
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
@ -49,39 +70,38 @@ export class InputControlVis extends Component {
|
|||
renderControls() {
|
||||
return this.props.controls.map((control, index) => {
|
||||
let controlComponent = null;
|
||||
switch (control.type) {
|
||||
case 'list':
|
||||
controlComponent = (
|
||||
<ListControl
|
||||
id={control.id}
|
||||
label={control.label}
|
||||
options={control.selectOptions}
|
||||
selectedOptions={control.value}
|
||||
formatOptionLabel={control.format}
|
||||
disableMsg={control.isEnabled() ? null : control.disabledReason}
|
||||
multiselect={control.options.multiselect}
|
||||
partialResults={control.partialResults}
|
||||
dynamicOptions={control.options.dynamicOptions}
|
||||
controlIndex={index}
|
||||
stageFilter={this.props.stageFilter}
|
||||
fetchOptions={query => {
|
||||
this.props.refreshControl(index, query);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'range':
|
||||
controlComponent = (
|
||||
<RangeControl
|
||||
control={control}
|
||||
controlIndex={index}
|
||||
stageFilter={this.props.stageFilter}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled control type ${control.type}`);
|
||||
|
||||
if (isListControl(control)) {
|
||||
controlComponent = (
|
||||
<ListControlComponent
|
||||
id={control.id}
|
||||
label={control.label}
|
||||
options={control.selectOptions}
|
||||
selectedOptions={control.value}
|
||||
formatOptionLabel={control.format}
|
||||
disableMsg={control.isEnabled() ? undefined : control.disabledReason}
|
||||
multiselect={control.options.multiselect}
|
||||
partialResults={control.partialResults}
|
||||
dynamicOptions={control.options.dynamicOptions}
|
||||
controlIndex={index}
|
||||
stageFilter={this.props.stageFilter}
|
||||
fetchOptions={query => {
|
||||
this.props.refreshControl(index, query);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (isRangeControl(control)) {
|
||||
controlComponent = (
|
||||
<RangeControlComponent
|
||||
control={control}
|
||||
controlIndex={index}
|
||||
stageFilter={this.props.stageFilter}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Unhandled control type ${control!.type}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem
|
||||
key={control.id}
|
||||
|
@ -152,15 +172,3 @@ export class InputControlVis extends Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
InputControlVis.propTypes = {
|
||||
stageFilter: PropTypes.func.isRequired,
|
||||
submitFilters: PropTypes.func.isRequired,
|
||||
resetControls: PropTypes.func.isRequired,
|
||||
clearControls: PropTypes.func.isRequired,
|
||||
controls: PropTypes.array.isRequired,
|
||||
updateFiltersOnChange: PropTypes.bool,
|
||||
hasChanges: PropTypes.func.isRequired,
|
||||
hasValues: PropTypes.func.isRequired,
|
||||
refreshControl: PropTypes.func.isRequired,
|
||||
};
|
|
@ -25,11 +25,11 @@ import { ListControl } from './list_control';
|
|||
|
||||
const options = ['choice1', 'choice2'];
|
||||
|
||||
const formatOptionLabel = value => {
|
||||
const formatOptionLabel = (value: any) => {
|
||||
return `${value} + formatting`;
|
||||
};
|
||||
|
||||
let stageFilter;
|
||||
let stageFilter: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
stageFilter = sinon.spy();
|
||||
|
@ -46,6 +46,7 @@ test('renders ListControl', () => {
|
|||
controlIndex={0}
|
||||
stageFilter={stageFilter}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
intl={{} as any}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot(); // eslint-disable-line
|
||||
|
@ -56,11 +57,13 @@ test('disableMsg', () => {
|
|||
<ListControl.WrappedComponent
|
||||
id="mock-list-control"
|
||||
label="list control"
|
||||
selectedOptions={[]}
|
||||
multiselect={true}
|
||||
controlIndex={0}
|
||||
stageFilter={stageFilter}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
disableMsg={'control is disabled to test rendering when disabled'}
|
||||
intl={{} as any}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot(); // eslint-disable-line
|
|
@ -17,46 +17,76 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { FormRow } from './form_row';
|
||||
import { injectI18n } from '@kbn/i18n/react';
|
||||
import { InjectedIntlProps } from 'react-intl';
|
||||
|
||||
import { EuiFieldText, EuiComboBox } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormRow } from './form_row';
|
||||
|
||||
interface ListControlUiState {
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export type ListControlUiProps = InjectedIntlProps & {
|
||||
id: string;
|
||||
label: string;
|
||||
selectedOptions: any[];
|
||||
options?: any[];
|
||||
formatOptionLabel: (option: any) => any;
|
||||
disableMsg?: string;
|
||||
multiselect?: boolean;
|
||||
dynamicOptions?: boolean;
|
||||
partialResults?: boolean;
|
||||
controlIndex: number;
|
||||
stageFilter: (controlIndex: number, value: any) => void;
|
||||
fetchOptions?: (searchValue: string) => void;
|
||||
};
|
||||
|
||||
class ListControlUi extends PureComponent<ListControlUiProps, ListControlUiState> {
|
||||
static defaultProps = {
|
||||
dynamicOptions: false,
|
||||
multiselect: true,
|
||||
selectedOptions: [],
|
||||
options: [],
|
||||
};
|
||||
|
||||
private isMounted: boolean = false;
|
||||
|
||||
class ListControlUi extends Component {
|
||||
state = {
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
this._isMounted = true;
|
||||
this.isMounted = true;
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this._isMounted = false;
|
||||
this.isMounted = false;
|
||||
};
|
||||
|
||||
handleOnChange = selectedOptions => {
|
||||
handleOnChange = (selectedOptions: any[]) => {
|
||||
const selectedValues = selectedOptions.map(({ value }) => {
|
||||
return value;
|
||||
});
|
||||
this.props.stageFilter(this.props.controlIndex, selectedValues);
|
||||
};
|
||||
|
||||
debouncedFetch = _.debounce(async searchValue => {
|
||||
await this.props.fetchOptions(searchValue);
|
||||
debouncedFetch = _.debounce(async (searchValue: string) => {
|
||||
if (this.props.fetchOptions) {
|
||||
await this.props.fetchOptions(searchValue);
|
||||
}
|
||||
|
||||
if (this._isMounted) {
|
||||
if (this.isMounted) {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
}, 300);
|
||||
|
||||
onSearchChange = searchValue => {
|
||||
onSearchChange = (searchValue: string) => {
|
||||
this.setState(
|
||||
{
|
||||
isLoading: true,
|
||||
|
@ -81,7 +111,7 @@ class ListControlUi extends Component {
|
|||
}
|
||||
|
||||
const options = this.props.options
|
||||
.map(option => {
|
||||
?.map(option => {
|
||||
return {
|
||||
label: this.props.formatOptionLabel(option).toString(),
|
||||
value: option,
|
||||
|
@ -141,29 +171,4 @@ class ListControlUi extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
ListControlUi.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
selectedOptions: PropTypes.array.isRequired,
|
||||
options: PropTypes.array,
|
||||
formatOptionLabel: PropTypes.func.isRequired,
|
||||
disableMsg: PropTypes.string,
|
||||
multiselect: PropTypes.bool,
|
||||
dynamicOptions: PropTypes.bool,
|
||||
partialResults: PropTypes.bool,
|
||||
controlIndex: PropTypes.number.isRequired,
|
||||
stageFilter: PropTypes.func.isRequired,
|
||||
fetchOptions: PropTypes.func,
|
||||
};
|
||||
|
||||
ListControlUi.defaultProps = {
|
||||
dynamicOptions: false,
|
||||
multiselect: true,
|
||||
};
|
||||
|
||||
ListControlUi.defaultProps = {
|
||||
selectedOptions: [],
|
||||
options: [],
|
||||
};
|
||||
|
||||
export const ListControl = injectI18n(ListControlUi);
|
|
@ -21,8 +21,11 @@ import React from 'react';
|
|||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
import { RangeControl, ceilWithPrecision, floorWithPrecision } from './range_control';
|
||||
import { RangeControl as RangeControlClass } from '../../control/range_control_factory';
|
||||
|
||||
const control = {
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
const control: RangeControlClass = {
|
||||
id: 'mock-range-control',
|
||||
isEnabled: () => {
|
||||
return true;
|
||||
|
@ -39,7 +42,7 @@ const control = {
|
|||
hasValue: () => {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
} as RangeControlClass;
|
||||
|
||||
test('renders RangeControl', () => {
|
||||
const component = shallowWithIntl(
|
||||
|
@ -49,7 +52,7 @@ test('renders RangeControl', () => {
|
|||
});
|
||||
|
||||
test('disabled', () => {
|
||||
const disabledRangeControl = {
|
||||
const disabledRangeControl: RangeControlClass = {
|
||||
id: 'mock-range-control',
|
||||
isEnabled: () => {
|
||||
return false;
|
||||
|
@ -64,7 +67,7 @@ test('disabled', () => {
|
|||
hasValue: () => {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
} as RangeControlClass;
|
||||
const component = shallowWithIntl(
|
||||
<RangeControl control={disabledRangeControl} controlIndex={0} stageFilter={() => {}} />
|
||||
);
|
|
@ -18,12 +18,17 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { FormRow } from './form_row';
|
||||
import { ValidatedDualRange } from 'ui/validated_range';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
function roundWithPrecision(value, decimalPlaces, roundFunction) {
|
||||
import { ValidatedDualRange } from '../../legacy_imports';
|
||||
import { FormRow } from './form_row';
|
||||
import { RangeControl as RangeControlClass } from '../../control/range_control_factory';
|
||||
|
||||
function roundWithPrecision(
|
||||
value: number,
|
||||
decimalPlaces: number,
|
||||
roundFunction: (n: number) => number
|
||||
) {
|
||||
if (decimalPlaces <= 0) {
|
||||
return roundFunction(value);
|
||||
}
|
||||
|
@ -35,18 +40,29 @@ function roundWithPrecision(value, decimalPlaces, roundFunction) {
|
|||
return results;
|
||||
}
|
||||
|
||||
export function ceilWithPrecision(value, decimalPlaces) {
|
||||
export function ceilWithPrecision(value: number, decimalPlaces: number) {
|
||||
return roundWithPrecision(value, decimalPlaces, Math.ceil);
|
||||
}
|
||||
|
||||
export function floorWithPrecision(value, decimalPlaces) {
|
||||
export function floorWithPrecision(value: number, decimalPlaces: number) {
|
||||
return roundWithPrecision(value, decimalPlaces, Math.floor);
|
||||
}
|
||||
|
||||
export class RangeControl extends Component {
|
||||
state = {};
|
||||
export interface RangeControlState {
|
||||
value?: [string, string];
|
||||
prevValue?: [string, string];
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
export interface RangeControlProps {
|
||||
control: RangeControlClass;
|
||||
controlIndex: number;
|
||||
stageFilter: (controlIndex: number, value: any) => void;
|
||||
}
|
||||
|
||||
export class RangeControl extends PureComponent<RangeControlProps, RangeControlState> {
|
||||
state: RangeControlState = {};
|
||||
|
||||
static getDerivedStateFromProps(nextProps: RangeControlProps, prevState: RangeControlState) {
|
||||
const nextValue = nextProps.control.hasValue()
|
||||
? [nextProps.control.value.min, nextProps.control.value.max]
|
||||
: ['', ''];
|
||||
|
@ -68,7 +84,7 @@ export class RangeControl extends Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
onChangeComplete = _.debounce(value => {
|
||||
onChangeComplete = _.debounce((value: [string, string]) => {
|
||||
const controlValue = {
|
||||
min: value[0],
|
||||
max: value[1],
|
||||
|
@ -111,16 +127,10 @@ export class RangeControl extends Component {
|
|||
id={this.props.control.id}
|
||||
label={this.props.control.label}
|
||||
controlIndex={this.props.controlIndex}
|
||||
disableMsg={this.props.control.isEnabled() ? null : this.props.control.disabledReason}
|
||||
disableMsg={this.props.control.isEnabled() ? undefined : this.props.control.disabledReason}
|
||||
>
|
||||
{this.renderControl()}
|
||||
</FormRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RangeControl.propTypes = {
|
||||
control: PropTypes.object.isRequired,
|
||||
controlIndex: PropTypes.number.isRequired,
|
||||
stageFilter: PropTypes.func.isRequired,
|
||||
};
|
|
@ -19,34 +19,50 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { Control } from './control';
|
||||
import { ControlParams } from '../editor_utils';
|
||||
import { FilterManager as BaseFilterManager } from './filter_manager/filter_manager';
|
||||
import { SearchSource } from '../legacy_imports';
|
||||
|
||||
function createControlParams(id, label) {
|
||||
function createControlParams(id: string, label: string): ControlParams {
|
||||
return {
|
||||
id: id,
|
||||
id,
|
||||
options: {},
|
||||
label: label,
|
||||
};
|
||||
label,
|
||||
} as ControlParams;
|
||||
}
|
||||
|
||||
let valueFromFilterBar;
|
||||
const mockFilterManager = {
|
||||
let valueFromFilterBar: any;
|
||||
const mockFilterManager: BaseFilterManager = {
|
||||
getValueFromFilterBar: () => {
|
||||
return valueFromFilterBar;
|
||||
},
|
||||
createFilter: value => {
|
||||
return `mockKbnFilter:${value}`;
|
||||
createFilter: (value: any) => {
|
||||
return `mockKbnFilter:${value}` as any;
|
||||
},
|
||||
getIndexPattern: () => {
|
||||
return 'mockIndexPattern';
|
||||
},
|
||||
};
|
||||
const mockKbnApi = {};
|
||||
} as any;
|
||||
|
||||
class ControlMock extends Control<BaseFilterManager> {
|
||||
fetch() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
}
|
||||
const mockKbnApi: SearchSource = {} as SearchSource;
|
||||
|
||||
describe('hasChanged', () => {
|
||||
let control;
|
||||
let control: ControlMock;
|
||||
|
||||
beforeEach(() => {
|
||||
control = new Control(createControlParams(3, 'control'), mockFilterManager, mockKbnApi);
|
||||
control = new ControlMock(
|
||||
createControlParams('3', 'control'),
|
||||
mockFilterManager,
|
||||
false,
|
||||
mockKbnApi
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -70,23 +86,26 @@ describe('hasChanged', () => {
|
|||
});
|
||||
|
||||
describe('ancestors', () => {
|
||||
let grandParentControl;
|
||||
let parentControl;
|
||||
let childControl;
|
||||
let grandParentControl: any;
|
||||
let parentControl: any;
|
||||
let childControl: any;
|
||||
beforeEach(() => {
|
||||
grandParentControl = new Control(
|
||||
createControlParams(1, 'grandparent control'),
|
||||
grandParentControl = new ControlMock(
|
||||
createControlParams('1', 'grandparent control'),
|
||||
mockFilterManager,
|
||||
false,
|
||||
mockKbnApi
|
||||
);
|
||||
parentControl = new Control(
|
||||
createControlParams(2, 'parent control'),
|
||||
parentControl = new ControlMock(
|
||||
createControlParams('2', 'parent control'),
|
||||
mockFilterManager,
|
||||
false,
|
||||
mockKbnApi
|
||||
);
|
||||
childControl = new Control(
|
||||
createControlParams(3, 'child control'),
|
||||
childControl = new ControlMock(
|
||||
createControlParams('3', 'child control'),
|
||||
mockFilterManager,
|
||||
false,
|
||||
mockKbnApi
|
||||
);
|
||||
});
|
||||
|
@ -122,7 +141,7 @@ describe('ancestors', () => {
|
|||
});
|
||||
|
||||
describe('getAncestorValues', () => {
|
||||
let lastAncestorValues;
|
||||
let lastAncestorValues: any[];
|
||||
beforeEach(() => {
|
||||
grandParentControl.set('myGrandParentValue');
|
||||
parentControl.set('myParentValue');
|
|
@ -22,32 +22,53 @@
|
|||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function noValuesDisableMsg(fieldName, indexPatternName) {
|
||||
import { esFilters } from '../../../../../plugins/data/public';
|
||||
import { SearchSource as SearchSourceClass } from '../legacy_imports';
|
||||
import { ControlParams, ControlParamsOptions, CONTROL_TYPES } from '../editor_utils';
|
||||
import { RangeFilterManager } from './filter_manager/range_filter_manager';
|
||||
import { PhraseFilterManager } from './filter_manager/phrase_filter_manager';
|
||||
import { FilterManager as BaseFilterManager } from './filter_manager/filter_manager';
|
||||
|
||||
export function noValuesDisableMsg(fieldName: string, indexPatternName: string) {
|
||||
return i18n.translate('inputControl.control.noValuesDisableTooltip', {
|
||||
defaultMessage:
|
||||
'Filtering occurs on the "{fieldName}" field, which doesn\'t exist on any documents in the "{indexPatternName}" \
|
||||
index pattern. Choose a different field or index documents that contain values for this field.',
|
||||
values: { fieldName: fieldName, indexPatternName: indexPatternName },
|
||||
values: { fieldName, indexPatternName },
|
||||
});
|
||||
}
|
||||
|
||||
export function noIndexPatternMsg(indexPatternId) {
|
||||
export function noIndexPatternMsg(indexPatternId: string) {
|
||||
return i18n.translate('inputControl.control.noIndexPatternTooltip', {
|
||||
defaultMessage: 'Could not locate index-pattern id: {indexPatternId}.',
|
||||
values: { indexPatternId },
|
||||
});
|
||||
}
|
||||
|
||||
export class Control {
|
||||
constructor(controlParams, filterManager, useTimeFilter, SearchSource) {
|
||||
export abstract class Control<FilterManager extends BaseFilterManager> {
|
||||
private kbnFilter: esFilters.Filter | null = null;
|
||||
|
||||
enable: boolean = false;
|
||||
disabledReason: string = '';
|
||||
value: any;
|
||||
|
||||
id: string;
|
||||
options: ControlParamsOptions;
|
||||
type: CONTROL_TYPES;
|
||||
label: string;
|
||||
ancestors: Array<Control<PhraseFilterManager | RangeFilterManager>> = [];
|
||||
|
||||
constructor(
|
||||
public controlParams: ControlParams,
|
||||
public filterManager: FilterManager,
|
||||
public useTimeFilter: boolean,
|
||||
public SearchSource: SearchSourceClass
|
||||
) {
|
||||
this.id = controlParams.id;
|
||||
this.controlParams = controlParams;
|
||||
this.options = controlParams.options;
|
||||
this.type = controlParams.type;
|
||||
this.label = controlParams.label ? controlParams.label : controlParams.fieldName;
|
||||
this.useTimeFilter = useTimeFilter;
|
||||
this.filterManager = filterManager;
|
||||
this.SearchSource = SearchSource;
|
||||
|
||||
// restore state from kibana filter context
|
||||
this.reset();
|
||||
|
@ -59,28 +80,20 @@ export class Control {
|
|||
);
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
throw new Error('fetch method not defined, subclass are required to implement');
|
||||
}
|
||||
abstract fetch(query: string): Promise<void>;
|
||||
|
||||
destroy() {
|
||||
throw new Error('destroy method not defined, subclass are required to implement');
|
||||
}
|
||||
abstract destroy(): void;
|
||||
|
||||
format = value => {
|
||||
format = (value: any) => {
|
||||
const field = this.filterManager.getField();
|
||||
if (field) {
|
||||
if (field?.format?.convert) {
|
||||
return field.format.convert(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param ancestors {array of Controls}
|
||||
*/
|
||||
setAncestors(ancestors) {
|
||||
setAncestors(ancestors: Array<Control<PhraseFilterManager | RangeFilterManager>>) {
|
||||
this.ancestors = ancestors;
|
||||
}
|
||||
|
||||
|
@ -110,17 +123,17 @@ export class Control {
|
|||
return this.enable;
|
||||
}
|
||||
|
||||
disable(reason) {
|
||||
disable(reason: string) {
|
||||
this.enable = false;
|
||||
this.disabledReason = reason;
|
||||
}
|
||||
|
||||
set(newValue) {
|
||||
set(newValue: any) {
|
||||
this.value = newValue;
|
||||
if (this.hasValue()) {
|
||||
this._kbnFilter = this.filterManager.createFilter(this.value);
|
||||
this.kbnFilter = this.filterManager.createFilter(this.value);
|
||||
} else {
|
||||
this._kbnFilter = null;
|
||||
this.kbnFilter = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,7 +141,7 @@ export class Control {
|
|||
* Remove any user changes to value by resetting value to that as provided by Kibana filter pills
|
||||
*/
|
||||
reset() {
|
||||
this._kbnFilter = null;
|
||||
this.kbnFilter = null;
|
||||
this.value = this.filterManager.getValueFromFilterBar();
|
||||
}
|
||||
|
||||
|
@ -144,17 +157,17 @@ export class Control {
|
|||
}
|
||||
|
||||
hasKbnFilter() {
|
||||
if (this._kbnFilter) {
|
||||
if (this.kbnFilter) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getKbnFilter() {
|
||||
return this._kbnFilter;
|
||||
return this.kbnFilter;
|
||||
}
|
||||
|
||||
hasValue() {
|
||||
hasValue(): boolean {
|
||||
return this.value !== undefined;
|
||||
}
|
||||
}
|
|
@ -19,14 +19,15 @@
|
|||
|
||||
import { rangeControlFactory } from './range_control_factory';
|
||||
import { listControlFactory } from './list_control_factory';
|
||||
import { ControlParams, CONTROL_TYPES } from '../editor_utils';
|
||||
|
||||
export function controlFactory(controlParams) {
|
||||
export function getControlFactory(controlParams: ControlParams) {
|
||||
let factory = null;
|
||||
switch (controlParams.type) {
|
||||
case 'range':
|
||||
case CONTROL_TYPES.RANGE:
|
||||
factory = rangeControlFactory;
|
||||
break;
|
||||
case 'list':
|
||||
case CONTROL_TYPES.LIST:
|
||||
factory = listControlFactory;
|
||||
break;
|
||||
default:
|
|
@ -16,15 +16,18 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
|
||||
import { esFilters, IndexPattern, TimefilterSetup } from '../../../../../plugins/data/public';
|
||||
import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports';
|
||||
|
||||
export function createSearchSource(
|
||||
SearchSource,
|
||||
initialState,
|
||||
indexPattern,
|
||||
aggs,
|
||||
useTimeFilter,
|
||||
filters = []
|
||||
SearchSource: SearchSourceClass,
|
||||
initialState: SearchSourceFields | null,
|
||||
indexPattern: IndexPattern,
|
||||
aggs: any,
|
||||
useTimeFilter: boolean,
|
||||
filters: esFilters.PhraseFilter[] = [],
|
||||
timefilter: TimefilterSetup['timefilter']
|
||||
) {
|
||||
const searchSource = initialState ? new SearchSource(initialState) : new SearchSource();
|
||||
// Do not not inherit from rootSearchSource to avoid picking up time and globals
|
||||
|
@ -32,7 +35,10 @@ export function createSearchSource(
|
|||
searchSource.setField('filter', () => {
|
||||
const activeFilters = [...filters];
|
||||
if (useTimeFilter) {
|
||||
activeFilters.push(timefilter.createFilter(indexPattern));
|
||||
const filter = timefilter.createFilter(indexPattern);
|
||||
if (filter) {
|
||||
activeFilters.push(filter);
|
||||
}
|
||||
}
|
||||
return activeFilters;
|
||||
});
|
|
@ -18,30 +18,45 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { FilterManager } from './filter_manager';
|
||||
import { coreMock } from '../../../../../../core/public/mocks';
|
||||
import {
|
||||
esFilters,
|
||||
IndexPattern,
|
||||
FilterManager as QueryFilterManager,
|
||||
} from '../../../../../../plugins/data/public';
|
||||
|
||||
const setupMock = coreMock.createSetup();
|
||||
|
||||
class FilterManagerTest extends FilterManager {
|
||||
createFilter() {
|
||||
return {} as esFilters.Filter;
|
||||
}
|
||||
|
||||
getValueFromFilterBar() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
describe('FilterManager', function() {
|
||||
const controlId = 'control1';
|
||||
|
||||
describe('findFilters', function() {
|
||||
const indexPatternMock = {};
|
||||
let kbnFilters;
|
||||
const queryFilterMock = {
|
||||
getAppFilters: () => {
|
||||
return kbnFilters;
|
||||
},
|
||||
getGlobalFilters: () => {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
let filterManager;
|
||||
const indexPatternMock = {} as IndexPattern;
|
||||
let kbnFilters: esFilters.Filter[];
|
||||
const queryFilterMock = new QueryFilterManager(setupMock.uiSettings);
|
||||
queryFilterMock.getAppFilters = () => kbnFilters;
|
||||
queryFilterMock.getGlobalFilters = () => [];
|
||||
|
||||
let filterManager: FilterManagerTest;
|
||||
beforeEach(() => {
|
||||
kbnFilters = [];
|
||||
filterManager = new FilterManager(controlId, 'field1', indexPatternMock, queryFilterMock);
|
||||
filterManager = new FilterManagerTest(controlId, 'field1', indexPatternMock, queryFilterMock);
|
||||
});
|
||||
|
||||
test('should not find filters that are not controlled by any visualization', function() {
|
||||
kbnFilters.push({});
|
||||
kbnFilters.push({} as esFilters.Filter);
|
||||
const foundFilters = filterManager.findFilters();
|
||||
expect(foundFilters.length).to.be(0);
|
||||
});
|
||||
|
@ -51,7 +66,7 @@ describe('FilterManager', function() {
|
|||
meta: {
|
||||
controlledBy: 'anotherControl',
|
||||
},
|
||||
});
|
||||
} as esFilters.Filter);
|
||||
const foundFilters = filterManager.findFilters();
|
||||
expect(foundFilters.length).to.be(0);
|
||||
});
|
||||
|
@ -61,7 +76,7 @@ describe('FilterManager', function() {
|
|||
meta: {
|
||||
controlledBy: controlId,
|
||||
},
|
||||
});
|
||||
} as esFilters.Filter);
|
||||
const foundFilters = filterManager.findFilters();
|
||||
expect(foundFilters.length).to.be(1);
|
||||
});
|
|
@ -19,15 +19,33 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
|
||||
export class FilterManager {
|
||||
constructor(controlId, fieldName, indexPattern, queryFilter) {
|
||||
this.controlId = controlId;
|
||||
this.fieldName = fieldName;
|
||||
this.indexPattern = indexPattern;
|
||||
this.queryFilter = queryFilter;
|
||||
}
|
||||
import {
|
||||
FilterManager as QueryFilterManager,
|
||||
IndexPattern,
|
||||
esFilters,
|
||||
} from '../../../../../../plugins/data/public';
|
||||
|
||||
getIndexPattern() {
|
||||
export abstract class FilterManager {
|
||||
constructor(
|
||||
public controlId: string,
|
||||
public fieldName: string,
|
||||
public indexPattern: IndexPattern,
|
||||
public queryFilter: QueryFilterManager
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Convert phrases into filter
|
||||
*
|
||||
* @param {any[]} phrases
|
||||
* @returns PhraseFilter
|
||||
* single phrase: match query
|
||||
* multiple phrases: bool query with should containing list of match_phrase queries
|
||||
*/
|
||||
abstract createFilter(phrases: any): esFilters.Filter;
|
||||
|
||||
abstract getValueFromFilterBar(): any;
|
||||
|
||||
getIndexPattern(): IndexPattern {
|
||||
return this.indexPattern;
|
||||
}
|
||||
|
||||
|
@ -35,11 +53,7 @@ export class FilterManager {
|
|||
return this.indexPattern.fields.getByName(this.fieldName);
|
||||
}
|
||||
|
||||
createFilter() {
|
||||
throw new Error('Must implement createFilter.');
|
||||
}
|
||||
|
||||
findFilters() {
|
||||
findFilters(): esFilters.Filter[] {
|
||||
const kbnFilters = _.flatten([
|
||||
this.queryFilter.getAppFilters(),
|
||||
this.queryFilter.getGlobalFilters(),
|
||||
|
@ -48,8 +62,4 @@ export class FilterManager {
|
|||
return _.get(kbnFilter, 'meta.controlledBy') === this.controlId;
|
||||
});
|
||||
}
|
||||
|
||||
getValueFromFilterBar() {
|
||||
throw new Error('Must implement getValueFromFilterBar.');
|
||||
}
|
||||
}
|
|
@ -18,6 +18,12 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import {
|
||||
esFilters,
|
||||
IndexPattern,
|
||||
FilterManager as QueryFilterManager,
|
||||
} from '../../../../../../plugins/data/public';
|
||||
import { PhraseFilterManager } from './phrase_filter_manager';
|
||||
|
||||
describe('PhraseFilterManager', function() {
|
||||
|
@ -28,22 +34,20 @@ describe('PhraseFilterManager', function() {
|
|||
const fieldMock = {
|
||||
name: 'field1',
|
||||
format: {
|
||||
convert: val => {
|
||||
return val;
|
||||
},
|
||||
convert: (value: any) => value,
|
||||
},
|
||||
};
|
||||
const indexPatternMock = {
|
||||
const indexPatternMock: IndexPattern = {
|
||||
id: indexPatternId,
|
||||
fields: {
|
||||
getByName: name => {
|
||||
const fields = { field1: fieldMock };
|
||||
getByName: (name: string) => {
|
||||
const fields: any = { field1: fieldMock };
|
||||
return fields[name];
|
||||
},
|
||||
},
|
||||
};
|
||||
const queryFilterMock = {};
|
||||
let filterManager;
|
||||
} as IndexPattern;
|
||||
const queryFilterMock: QueryFilterManager = {} as QueryFilterManager;
|
||||
let filterManager: PhraseFilterManager;
|
||||
beforeEach(() => {
|
||||
filterManager = new PhraseFilterManager(
|
||||
controlId,
|
||||
|
@ -83,22 +87,32 @@ describe('PhraseFilterManager', function() {
|
|||
});
|
||||
|
||||
describe('getValueFromFilterBar', function() {
|
||||
const indexPatternMock = {};
|
||||
const queryFilterMock = {};
|
||||
let filterManager;
|
||||
beforeEach(() => {
|
||||
class MockFindFiltersPhraseFilterManager extends PhraseFilterManager {
|
||||
constructor(controlId, fieldName, indexPattern, queryFilter, delimiter) {
|
||||
super(controlId, fieldName, indexPattern, queryFilter, delimiter);
|
||||
this.mockFilters = [];
|
||||
}
|
||||
findFilters() {
|
||||
return this.mockFilters;
|
||||
}
|
||||
setMockFilters(mockFilters) {
|
||||
this.mockFilters = mockFilters;
|
||||
}
|
||||
class MockFindFiltersPhraseFilterManager extends PhraseFilterManager {
|
||||
mockFilters: esFilters.Filter[];
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
fieldName: string,
|
||||
indexPattern: IndexPattern,
|
||||
queryFilter: QueryFilterManager
|
||||
) {
|
||||
super(id, fieldName, indexPattern, queryFilter);
|
||||
this.mockFilters = [];
|
||||
}
|
||||
|
||||
findFilters() {
|
||||
return this.mockFilters;
|
||||
}
|
||||
|
||||
setMockFilters(mockFilters: esFilters.Filter[]) {
|
||||
this.mockFilters = mockFilters;
|
||||
}
|
||||
}
|
||||
|
||||
const indexPatternMock: IndexPattern = {} as IndexPattern;
|
||||
const queryFilterMock: QueryFilterManager = {} as QueryFilterManager;
|
||||
let filterManager: MockFindFiltersPhraseFilterManager;
|
||||
beforeEach(() => {
|
||||
filterManager = new MockFindFiltersPhraseFilterManager(
|
||||
controlId,
|
||||
'field1',
|
||||
|
@ -119,7 +133,7 @@ describe('PhraseFilterManager', function() {
|
|||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
] as esFilters.Filter[]);
|
||||
expect(filterManager.getValueFromFilterBar()).to.eql(['ios']);
|
||||
});
|
||||
|
||||
|
@ -145,7 +159,7 @@ describe('PhraseFilterManager', function() {
|
|||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
] as esFilters.Filter[]);
|
||||
expect(filterManager.getValueFromFilterBar()).to.eql(['ios', 'win xp']);
|
||||
});
|
||||
|
||||
|
@ -169,7 +183,7 @@ describe('PhraseFilterManager', function() {
|
|||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
] as esFilters.Filter[]);
|
||||
expect(filterManager.getValueFromFilterBar()).to.eql(['ios', 'win xp']);
|
||||
});
|
||||
|
||||
|
@ -185,7 +199,7 @@ describe('PhraseFilterManager', function() {
|
|||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
] as esFilters.Filter[]);
|
||||
expect(filterManager.getValueFromFilterBar()).to.eql(undefined);
|
||||
});
|
||||
});
|
|
@ -18,37 +18,38 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { FilterManager } from './filter_manager.js';
|
||||
import { esFilters } from '../../../../../../plugins/data/public';
|
||||
|
||||
import { FilterManager } from './filter_manager';
|
||||
import {
|
||||
esFilters,
|
||||
IndexPattern,
|
||||
FilterManager as QueryFilterManager,
|
||||
} from '../../../../../../plugins/data/public';
|
||||
|
||||
export class PhraseFilterManager extends FilterManager {
|
||||
constructor(controlId, fieldName, indexPattern, queryFilter) {
|
||||
constructor(
|
||||
controlId: string,
|
||||
fieldName: string,
|
||||
indexPattern: IndexPattern,
|
||||
queryFilter: QueryFilterManager
|
||||
) {
|
||||
super(controlId, fieldName, indexPattern, queryFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert phrases into filter
|
||||
*
|
||||
* @param {array} phrases
|
||||
* @return {object} query filter
|
||||
* single phrase: match query
|
||||
* multiple phrases: bool query with should containing list of match_phrase queries
|
||||
*/
|
||||
createFilter(phrases) {
|
||||
let newFilter;
|
||||
if (phrases.length === 1) {
|
||||
newFilter = esFilters.buildPhraseFilter(
|
||||
this.indexPattern.fields.getByName(this.fieldName),
|
||||
phrases[0],
|
||||
this.indexPattern
|
||||
);
|
||||
} else {
|
||||
newFilter = esFilters.buildPhrasesFilter(
|
||||
this.indexPattern.fields.getByName(this.fieldName),
|
||||
phrases,
|
||||
this.indexPattern
|
||||
);
|
||||
createFilter(phrases: any): esFilters.PhraseFilter {
|
||||
let newFilter: esFilters.PhraseFilter;
|
||||
const value = this.indexPattern.fields.getByName(this.fieldName);
|
||||
|
||||
if (!value) {
|
||||
throw new Error(`Unable to find field with name: ${this.fieldName} on indexPattern`);
|
||||
}
|
||||
|
||||
if (phrases.length === 1) {
|
||||
newFilter = esFilters.buildPhraseFilter(value, phrases[0], this.indexPattern);
|
||||
} else {
|
||||
newFilter = esFilters.buildPhrasesFilter(value, phrases, this.indexPattern);
|
||||
}
|
||||
|
||||
newFilter.meta.key = this.fieldName;
|
||||
newFilter.meta.controlledBy = this.controlId;
|
||||
return newFilter;
|
||||
|
@ -62,7 +63,7 @@ export class PhraseFilterManager extends FilterManager {
|
|||
|
||||
const values = kbnFilters
|
||||
.map(kbnFilter => {
|
||||
return this._getValueFromFilter(kbnFilter);
|
||||
return this.getValueFromFilter(kbnFilter);
|
||||
})
|
||||
.filter(value => value != null);
|
||||
|
||||
|
@ -78,15 +79,15 @@ export class PhraseFilterManager extends FilterManager {
|
|||
/**
|
||||
* Extract filtering value from kibana filters
|
||||
*
|
||||
* @param {object} kbnFilter
|
||||
* @param {esFilters.PhraseFilter} kbnFilter
|
||||
* @return {Array.<string>} array of values pulled from filter
|
||||
*/
|
||||
_getValueFromFilter(kbnFilter) {
|
||||
private getValueFromFilter(kbnFilter: esFilters.PhraseFilter): any {
|
||||
// bool filter - multiple phrase filters
|
||||
if (_.has(kbnFilter, 'query.bool.should')) {
|
||||
return _.get(kbnFilter, 'query.bool.should')
|
||||
.map(kbnFilter => {
|
||||
return this._getValueFromFilter(kbnFilter);
|
||||
return _.get<esFilters.PhraseFilter[]>(kbnFilter, 'query.bool.should')
|
||||
.map(kbnQueryFilter => {
|
||||
return this.getValueFromFilter(kbnQueryFilter);
|
||||
})
|
||||
.filter(value => {
|
||||
if (value) {
|
|
@ -18,7 +18,13 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { RangeFilterManager } from './range_filter_manager';
|
||||
import {
|
||||
esFilters,
|
||||
IndexPattern,
|
||||
FilterManager as QueryFilterManager,
|
||||
} from '../../../../../../plugins/data/public';
|
||||
|
||||
describe('RangeFilterManager', function() {
|
||||
const controlId = 'control1';
|
||||
|
@ -28,19 +34,19 @@ describe('RangeFilterManager', function() {
|
|||
const fieldMock = {
|
||||
name: 'field1',
|
||||
};
|
||||
const indexPatternMock = {
|
||||
const indexPatternMock: IndexPattern = {
|
||||
id: indexPatternId,
|
||||
fields: {
|
||||
getByName: name => {
|
||||
const fields = {
|
||||
getByName: (name: any) => {
|
||||
const fields: any = {
|
||||
field1: fieldMock,
|
||||
};
|
||||
return fields[name];
|
||||
},
|
||||
},
|
||||
};
|
||||
const queryFilterMock = {};
|
||||
let filterManager;
|
||||
} as IndexPattern;
|
||||
const queryFilterMock: QueryFilterManager = {} as QueryFilterManager;
|
||||
let filterManager: RangeFilterManager;
|
||||
beforeEach(() => {
|
||||
filterManager = new RangeFilterManager(
|
||||
controlId,
|
||||
|
@ -62,22 +68,32 @@ describe('RangeFilterManager', function() {
|
|||
});
|
||||
|
||||
describe('getValueFromFilterBar', function() {
|
||||
const indexPatternMock = {};
|
||||
const queryFilterMock = {};
|
||||
let filterManager;
|
||||
beforeEach(() => {
|
||||
class MockFindFiltersRangeFilterManager extends RangeFilterManager {
|
||||
constructor(controlId, fieldName, indexPattern, queryFilter) {
|
||||
super(controlId, fieldName, indexPattern, queryFilter);
|
||||
this.mockFilters = [];
|
||||
}
|
||||
findFilters() {
|
||||
return this.mockFilters;
|
||||
}
|
||||
setMockFilters(mockFilters) {
|
||||
this.mockFilters = mockFilters;
|
||||
}
|
||||
class MockFindFiltersRangeFilterManager extends RangeFilterManager {
|
||||
mockFilters: esFilters.RangeFilter[];
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
fieldName: string,
|
||||
indexPattern: IndexPattern,
|
||||
queryFilter: QueryFilterManager
|
||||
) {
|
||||
super(id, fieldName, indexPattern, queryFilter);
|
||||
this.mockFilters = [];
|
||||
}
|
||||
|
||||
findFilters() {
|
||||
return this.mockFilters;
|
||||
}
|
||||
|
||||
setMockFilters(mockFilters: esFilters.RangeFilter[]) {
|
||||
this.mockFilters = mockFilters;
|
||||
}
|
||||
}
|
||||
|
||||
const indexPatternMock: IndexPattern = {} as IndexPattern;
|
||||
const queryFilterMock: QueryFilterManager = {} as QueryFilterManager;
|
||||
let filterManager: MockFindFiltersRangeFilterManager;
|
||||
beforeEach(() => {
|
||||
filterManager = new MockFindFiltersRangeFilterManager(
|
||||
controlId,
|
||||
'field1',
|
||||
|
@ -95,14 +111,15 @@ describe('RangeFilterManager', function() {
|
|||
lt: 3,
|
||||
},
|
||||
},
|
||||
meta: {} as esFilters.RangeFilterMeta,
|
||||
},
|
||||
]);
|
||||
] as esFilters.RangeFilter[]);
|
||||
const value = filterManager.getValueFromFilterBar();
|
||||
expect(value).to.be.a('object');
|
||||
expect(value).to.have.property('min');
|
||||
expect(value.min).to.be(1);
|
||||
expect(value?.min).to.be(1);
|
||||
expect(value).to.have.property('max');
|
||||
expect(value.max).to.be(3);
|
||||
expect(value?.max).to.be(3);
|
||||
});
|
||||
|
||||
test('should return undefined when filter value can not be extracted from Kibana filter', function() {
|
||||
|
@ -114,8 +131,9 @@ describe('RangeFilterManager', function() {
|
|||
lte: 3,
|
||||
},
|
||||
},
|
||||
meta: {} as esFilters.RangeFilterMeta,
|
||||
},
|
||||
]);
|
||||
] as esFilters.RangeFilter[]);
|
||||
expect(filterManager.getValueFromFilterBar()).to.eql(undefined);
|
||||
});
|
||||
});
|
|
@ -18,11 +18,17 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { FilterManager } from './filter_manager.js';
|
||||
import { esFilters } from '../../../../../../plugins/data/public';
|
||||
|
||||
import { FilterManager } from './filter_manager';
|
||||
import { esFilters, IFieldType } from '../../../../../../plugins/data/public';
|
||||
|
||||
interface SliderValue {
|
||||
min?: string | number;
|
||||
max?: string | number;
|
||||
}
|
||||
|
||||
// Convert slider value into ES range filter
|
||||
function toRange(sliderValue) {
|
||||
function toRange(sliderValue: SliderValue) {
|
||||
return {
|
||||
gte: sliderValue.min,
|
||||
lte: sliderValue.max,
|
||||
|
@ -30,8 +36,8 @@ function toRange(sliderValue) {
|
|||
}
|
||||
|
||||
// Convert ES range filter into slider value
|
||||
function fromRange(range) {
|
||||
const sliderValue = {};
|
||||
function fromRange(range: esFilters.RangeFilterParams): SliderValue {
|
||||
const sliderValue: SliderValue = {};
|
||||
if (_.has(range, 'gte')) {
|
||||
sliderValue.min = _.get(range, 'gte');
|
||||
}
|
||||
|
@ -54,9 +60,10 @@ export class RangeFilterManager extends FilterManager {
|
|||
* @param {object} react-input-range value - POJO with `min` and `max` properties
|
||||
* @return {object} range filter
|
||||
*/
|
||||
createFilter(value) {
|
||||
createFilter(value: SliderValue): esFilters.RangeFilter {
|
||||
const newFilter = esFilters.buildRangeFilter(
|
||||
this.indexPattern.fields.getByName(this.fieldName),
|
||||
// TODO: Fix type to be required
|
||||
this.indexPattern.fields.getByName(this.fieldName) as IFieldType,
|
||||
toRange(value),
|
||||
this.indexPattern
|
||||
);
|
||||
|
@ -65,13 +72,13 @@ export class RangeFilterManager extends FilterManager {
|
|||
return newFilter;
|
||||
}
|
||||
|
||||
getValueFromFilterBar() {
|
||||
getValueFromFilterBar(): SliderValue | undefined {
|
||||
const kbnFilters = this.findFilters();
|
||||
if (kbnFilters.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let range;
|
||||
let range: esFilters.RangeFilterParams;
|
||||
if (_.has(kbnFilters[0], 'script')) {
|
||||
range = _.get(kbnFilters[0], 'script.script.params');
|
||||
} else {
|
|
@ -17,92 +17,33 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { listControlFactory } from './list_control_factory';
|
||||
import { listControlFactory, ListControl } from './list_control_factory';
|
||||
import { ControlParams, CONTROL_TYPES } from '../editor_utils';
|
||||
import { getDepsMock } from '../components/editor/__tests__/get_deps_mock';
|
||||
import { getSearchSourceMock } from '../components/editor/__tests__/get_search_service_mock';
|
||||
|
||||
jest.mock('ui/timefilter', () => ({
|
||||
createFilter: jest.fn(),
|
||||
const MockSearchSource = getSearchSourceMock();
|
||||
const deps = getDepsMock();
|
||||
|
||||
jest.doMock('./create_search_source.ts', () => ({
|
||||
createSearchSource: MockSearchSource,
|
||||
}));
|
||||
|
||||
jest.mock('ui/new_platform', () => ({
|
||||
npStart: {
|
||||
plugins: {
|
||||
data: {
|
||||
query: {
|
||||
filterManager: {
|
||||
fieldName: 'myNumberField',
|
||||
getIndexPattern: () => ({
|
||||
fields: {
|
||||
getByName: name => {
|
||||
const fields = { myField: { name: 'myField' } };
|
||||
return fields[name];
|
||||
},
|
||||
},
|
||||
}),
|
||||
getAppFilters: jest.fn().mockImplementation(() => []),
|
||||
getGlobalFilters: jest.fn().mockImplementation(() => []),
|
||||
},
|
||||
},
|
||||
indexPatterns: {
|
||||
get: () => ({
|
||||
fields: {
|
||||
getByName: name => {
|
||||
const fields = { myField: { name: 'myField' } };
|
||||
return fields[name];
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
chrome.getInjected.mockImplementation(key => {
|
||||
switch (key) {
|
||||
case 'autocompleteTimeout':
|
||||
return 1000;
|
||||
case 'autocompleteTerminateAfter':
|
||||
return 100000;
|
||||
}
|
||||
});
|
||||
|
||||
function MockSearchSource() {
|
||||
return {
|
||||
setParent: () => {},
|
||||
setField: () => {},
|
||||
fetch: async () => {
|
||||
return {
|
||||
aggregations: {
|
||||
termsAgg: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'Zurich Airport',
|
||||
doc_count: 691,
|
||||
},
|
||||
{
|
||||
key: 'Xi an Xianyang International Airport',
|
||||
doc_count: 526,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('hasValue', () => {
|
||||
const controlParams = {
|
||||
const controlParams: ControlParams = {
|
||||
id: '1',
|
||||
fieldName: 'myField',
|
||||
options: {},
|
||||
options: {} as any,
|
||||
type: CONTROL_TYPES.LIST,
|
||||
label: 'test',
|
||||
indexPattern: {} as any,
|
||||
parent: 'parent',
|
||||
};
|
||||
const useTimeFilter = false;
|
||||
|
||||
let listControl;
|
||||
let listControl: ListControl;
|
||||
beforeEach(async () => {
|
||||
listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource);
|
||||
listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource, deps);
|
||||
});
|
||||
|
||||
test('should be false when control has no value', () => {
|
||||
|
@ -121,22 +62,25 @@ describe('hasValue', () => {
|
|||
});
|
||||
|
||||
describe('fetch', () => {
|
||||
const controlParams = {
|
||||
const controlParams: ControlParams = {
|
||||
id: '1',
|
||||
fieldName: 'myField',
|
||||
options: {},
|
||||
options: {} as any,
|
||||
type: CONTROL_TYPES.LIST,
|
||||
label: 'test',
|
||||
indexPattern: {} as any,
|
||||
parent: 'parent',
|
||||
};
|
||||
const useTimeFilter = false;
|
||||
const SearchSource = jest.fn(MockSearchSource);
|
||||
|
||||
let listControl;
|
||||
let listControl: ListControl;
|
||||
beforeEach(async () => {
|
||||
listControl = await listControlFactory(controlParams, useTimeFilter, SearchSource);
|
||||
listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource, deps);
|
||||
});
|
||||
|
||||
test('should pass in timeout parameters from injected vars', async () => {
|
||||
await listControl.fetch();
|
||||
expect(SearchSource).toHaveBeenCalledWith({
|
||||
expect(MockSearchSource).toHaveBeenCalledWith({
|
||||
timeout: `1000ms`,
|
||||
terminate_after: 100000,
|
||||
});
|
||||
|
@ -152,24 +96,37 @@ describe('fetch', () => {
|
|||
});
|
||||
|
||||
describe('fetch with ancestors', () => {
|
||||
const controlParams = {
|
||||
const controlParams: ControlParams = {
|
||||
id: '1',
|
||||
fieldName: 'myField',
|
||||
options: {},
|
||||
options: {} as any,
|
||||
type: CONTROL_TYPES.LIST,
|
||||
label: 'test',
|
||||
indexPattern: {} as any,
|
||||
parent: 'parent',
|
||||
};
|
||||
const useTimeFilter = false;
|
||||
|
||||
let listControl;
|
||||
let listControl: ListControl;
|
||||
let parentControl;
|
||||
beforeEach(async () => {
|
||||
listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource);
|
||||
listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource, deps);
|
||||
|
||||
const parentControlParams = {
|
||||
const parentControlParams: ControlParams = {
|
||||
id: 'parent',
|
||||
fieldName: 'myField',
|
||||
options: {},
|
||||
options: {} as any,
|
||||
type: CONTROL_TYPES.LIST,
|
||||
label: 'test',
|
||||
indexPattern: {} as any,
|
||||
parent: 'parent',
|
||||
};
|
||||
parentControl = await listControlFactory(parentControlParams, useTimeFilter, MockSearchSource);
|
||||
parentControl = await listControlFactory(
|
||||
parentControlParams,
|
||||
useTimeFilter,
|
||||
MockSearchSource,
|
||||
deps
|
||||
);
|
||||
parentControl.clear();
|
||||
listControl.setAncestors([parentControl]);
|
||||
});
|
|
@ -18,20 +18,30 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports';
|
||||
import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control';
|
||||
import { PhraseFilterManager } from './filter_manager/phrase_filter_manager';
|
||||
import { createSearchSource } from './create_search_source';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import chrome from 'ui/chrome';
|
||||
import { ControlParams } from '../editor_utils';
|
||||
import { InputControlVisDependencies } from '../plugin';
|
||||
import { IFieldType, TimefilterSetup } from '../../../../../plugins/data/public';
|
||||
|
||||
function getEscapedQuery(query = '') {
|
||||
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators
|
||||
return query.replace(/[.?+*|{}[\]()"\\#@&<>~]/g, match => `\\${match}`);
|
||||
}
|
||||
|
||||
const termsAgg = ({ field, size, direction, query }) => {
|
||||
const terms = {
|
||||
interface TermsAggArgs {
|
||||
field?: IFieldType;
|
||||
size: number | null;
|
||||
direction: string;
|
||||
query?: string;
|
||||
}
|
||||
|
||||
const termsAgg = ({ field, size, direction, query }: TermsAggArgs) => {
|
||||
const terms: any = {
|
||||
order: {
|
||||
_count: direction,
|
||||
},
|
||||
|
@ -41,14 +51,14 @@ const termsAgg = ({ field, size, direction, query }) => {
|
|||
terms.size = size < 1 ? 1 : size;
|
||||
}
|
||||
|
||||
if (field.scripted) {
|
||||
if (field?.scripted) {
|
||||
terms.script = {
|
||||
source: field.script,
|
||||
lang: field.lang,
|
||||
};
|
||||
terms.value_type = field.type === 'number' ? 'float' : field.type;
|
||||
} else {
|
||||
terms.field = field.name;
|
||||
terms.field = field?.name;
|
||||
}
|
||||
|
||||
if (query) {
|
||||
|
@ -57,13 +67,34 @@ const termsAgg = ({ field, size, direction, query }) => {
|
|||
|
||||
return {
|
||||
termsAgg: {
|
||||
terms: terms,
|
||||
terms,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
class ListControl extends Control {
|
||||
fetch = async query => {
|
||||
export class ListControl extends Control<PhraseFilterManager> {
|
||||
private getInjectedVar: InputControlVisDependencies['core']['injectedMetadata']['getInjectedVar'];
|
||||
private timefilter: TimefilterSetup['timefilter'];
|
||||
|
||||
abortController?: AbortController;
|
||||
lastAncestorValues: any;
|
||||
lastQuery?: string;
|
||||
partialResults?: boolean;
|
||||
selectOptions?: string[];
|
||||
|
||||
constructor(
|
||||
controlParams: ControlParams,
|
||||
filterManager: PhraseFilterManager,
|
||||
useTimeFilter: boolean,
|
||||
SearchSource: SearchSourceClass,
|
||||
deps: InputControlVisDependencies
|
||||
) {
|
||||
super(controlParams, filterManager, useTimeFilter, SearchSource);
|
||||
this.getInjectedVar = deps.core.injectedMetadata.getInjectedVar;
|
||||
this.timefilter = deps.data.query.timefilter.timefilter;
|
||||
}
|
||||
|
||||
fetch = async (query?: string) => {
|
||||
// Abort any in-progress fetch
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
|
@ -101,9 +132,9 @@ class ListControl extends Control {
|
|||
}
|
||||
|
||||
const fieldName = this.filterManager.fieldName;
|
||||
const initialSearchSourceState = {
|
||||
timeout: `${chrome.getInjected('autocompleteTimeout')}ms`,
|
||||
terminate_after: chrome.getInjected('autocompleteTerminateAfter'),
|
||||
const initialSearchSourceState: SearchSourceFields = {
|
||||
timeout: `${this.getInjectedVar('autocompleteTimeout')}ms`,
|
||||
terminate_after: Number(this.getInjectedVar('autocompleteTerminateAfter')),
|
||||
};
|
||||
const aggs = termsAgg({
|
||||
field: indexPattern.fields.getByName(fieldName),
|
||||
|
@ -117,7 +148,8 @@ class ListControl extends Control {
|
|||
indexPattern,
|
||||
aggs,
|
||||
this.useTimeFilter,
|
||||
ancestorFilters
|
||||
ancestorFilters,
|
||||
this.timefilter
|
||||
);
|
||||
const abortSignal = this.abortController.signal;
|
||||
|
||||
|
@ -143,8 +175,8 @@ class ListControl extends Control {
|
|||
return;
|
||||
}
|
||||
|
||||
const selectOptions = _.get(resp, 'aggregations.termsAgg.buckets', []).map(bucket => {
|
||||
return bucket.key;
|
||||
const selectOptions = _.get(resp, 'aggregations.termsAgg.buckets', []).map((bucket: any) => {
|
||||
return bucket?.key;
|
||||
});
|
||||
|
||||
if (selectOptions.length === 0 && !query) {
|
||||
|
@ -167,29 +199,34 @@ class ListControl extends Control {
|
|||
}
|
||||
}
|
||||
|
||||
export async function listControlFactory(controlParams, useTimeFilter, SearchSource) {
|
||||
let indexPattern;
|
||||
try {
|
||||
indexPattern = await npStart.plugins.data.indexPatterns.get(controlParams.indexPattern);
|
||||
export async function listControlFactory(
|
||||
controlParams: ControlParams,
|
||||
useTimeFilter: boolean,
|
||||
SearchSource: SearchSourceClass,
|
||||
deps: InputControlVisDependencies
|
||||
) {
|
||||
const [, { data: dataPluginStart }] = await deps.core.getStartServices();
|
||||
const indexPattern = await dataPluginStart.indexPatterns.get(controlParams.indexPattern);
|
||||
|
||||
// dynamic options are only allowed on String fields but the setting defaults to true so it could
|
||||
// be enabled for non-string fields (since UI input is hidden for non-string fields).
|
||||
// If field is not string, then disable dynamic options.
|
||||
const field = indexPattern.fields.find(field => {
|
||||
return field.name === controlParams.fieldName;
|
||||
});
|
||||
if (field && field.type !== 'string') {
|
||||
controlParams.options.dynamicOptions = false;
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore not found error and return control so it can be displayed in disabled state.
|
||||
// dynamic options are only allowed on String fields but the setting defaults to true so it could
|
||||
// be enabled for non-string fields (since UI input is hidden for non-string fields).
|
||||
// If field is not string, then disable dynamic options.
|
||||
const field = indexPattern.fields.find(({ name }) => name === controlParams.fieldName);
|
||||
if (field && field.type !== 'string') {
|
||||
controlParams.options.dynamicOptions = false;
|
||||
}
|
||||
|
||||
const { filterManager } = npStart.plugins.data.query;
|
||||
return new ListControl(
|
||||
const listControl = new ListControl(
|
||||
controlParams,
|
||||
new PhraseFilterManager(controlParams.id, controlParams.fieldName, indexPattern, filterManager),
|
||||
new PhraseFilterManager(
|
||||
controlParams.id,
|
||||
controlParams.fieldName,
|
||||
indexPattern,
|
||||
deps.data.query.filterManager
|
||||
),
|
||||
useTimeFilter,
|
||||
SearchSource
|
||||
SearchSource,
|
||||
deps
|
||||
);
|
||||
return listControl;
|
||||
}
|
|
@ -18,74 +18,37 @@
|
|||
*/
|
||||
|
||||
import { rangeControlFactory } from './range_control_factory';
|
||||
import { ControlParams, CONTROL_TYPES } from '../editor_utils';
|
||||
import { getSearchSourceMock } from '../components/editor/__tests__/get_search_service_mock';
|
||||
import { getDepsMock } from '../components/editor/__tests__/get_deps_mock';
|
||||
|
||||
let esSearchResponse;
|
||||
class MockSearchSource {
|
||||
setParent() {}
|
||||
setField() {}
|
||||
async fetch() {
|
||||
return esSearchResponse;
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('ui/timefilter', () => ({
|
||||
createFilter: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('ui/new_platform', () => ({
|
||||
npStart: {
|
||||
plugins: {
|
||||
data: {
|
||||
query: {
|
||||
filterManager: {
|
||||
fieldName: 'myNumberField',
|
||||
getIndexPattern: () => ({
|
||||
fields: {
|
||||
getByName: name => {
|
||||
const fields = { myNumberField: { name: 'myNumberField' } };
|
||||
return fields[name];
|
||||
},
|
||||
},
|
||||
}),
|
||||
getAppFilters: jest.fn().mockImplementation(() => []),
|
||||
getGlobalFilters: jest.fn().mockImplementation(() => []),
|
||||
},
|
||||
},
|
||||
indexPatterns: {
|
||||
get: () => ({
|
||||
fields: {
|
||||
getByName: name => {
|
||||
const fields = { myNumberField: { name: 'myNumberField' } };
|
||||
return fields[name];
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
const deps = getDepsMock();
|
||||
|
||||
describe('fetch', () => {
|
||||
const controlParams = {
|
||||
const controlParams: ControlParams = {
|
||||
id: '1',
|
||||
fieldName: 'myNumberField',
|
||||
options: {},
|
||||
type: CONTROL_TYPES.RANGE,
|
||||
label: 'test',
|
||||
indexPattern: {} as any,
|
||||
parent: {} as any,
|
||||
};
|
||||
const useTimeFilter = false;
|
||||
|
||||
let rangeControl;
|
||||
beforeEach(async () => {
|
||||
rangeControl = await rangeControlFactory(controlParams, useTimeFilter, MockSearchSource);
|
||||
});
|
||||
|
||||
test('should set min and max from aggregation results', async () => {
|
||||
esSearchResponse = {
|
||||
const esSearchResponse = {
|
||||
aggregations: {
|
||||
maxAgg: { value: 100 },
|
||||
minAgg: { value: 10 },
|
||||
},
|
||||
};
|
||||
const rangeControl = await rangeControlFactory(
|
||||
controlParams,
|
||||
useTimeFilter,
|
||||
getSearchSourceMock(esSearchResponse),
|
||||
deps
|
||||
);
|
||||
await rangeControl.fetch();
|
||||
|
||||
expect(rangeControl.isEnabled()).toBe(true);
|
||||
|
@ -95,12 +58,18 @@ describe('fetch', () => {
|
|||
|
||||
test('should disable control when there are 0 hits', async () => {
|
||||
// ES response when the query does not match any documents
|
||||
esSearchResponse = {
|
||||
const esSearchResponse = {
|
||||
aggregations: {
|
||||
maxAgg: { value: null },
|
||||
minAgg: { value: null },
|
||||
},
|
||||
};
|
||||
const rangeControl = await rangeControlFactory(
|
||||
controlParams,
|
||||
useTimeFilter,
|
||||
getSearchSourceMock(esSearchResponse),
|
||||
deps
|
||||
);
|
||||
await rangeControl.fetch();
|
||||
|
||||
expect(rangeControl.isEnabled()).toBe(false);
|
||||
|
@ -109,7 +78,13 @@ describe('fetch', () => {
|
|||
test('should disable control when response is empty', async () => {
|
||||
// ES response for dashboardonly user who does not have read permissions on index is 200 (which is weird)
|
||||
// and there is not aggregations key
|
||||
esSearchResponse = {};
|
||||
const esSearchResponse = {};
|
||||
const rangeControl = await rangeControlFactory(
|
||||
controlParams,
|
||||
useTimeFilter,
|
||||
getSearchSourceMock(esSearchResponse),
|
||||
deps
|
||||
);
|
||||
await rangeControl.fetch();
|
||||
|
||||
expect(rangeControl.isEnabled()).toBe(false);
|
|
@ -18,22 +18,29 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { SearchSource as SearchSourceClass } from '../legacy_imports';
|
||||
import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control';
|
||||
import { RangeFilterManager } from './filter_manager/range_filter_manager';
|
||||
import { createSearchSource } from './create_search_source';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import { ControlParams } from '../editor_utils';
|
||||
import { InputControlVisDependencies } from '../plugin';
|
||||
import { IFieldType, TimefilterSetup } from '../.../../../../../../plugins/data/public';
|
||||
|
||||
const minMaxAgg = field => {
|
||||
const aggBody = {};
|
||||
if (field.scripted) {
|
||||
aggBody.script = {
|
||||
source: field.script,
|
||||
lang: field.lang,
|
||||
};
|
||||
} else {
|
||||
aggBody.field = field.name;
|
||||
const minMaxAgg = (field?: IFieldType) => {
|
||||
const aggBody: any = {};
|
||||
if (field) {
|
||||
if (field.scripted) {
|
||||
aggBody.script = {
|
||||
source: field.script,
|
||||
lang: field.lang,
|
||||
};
|
||||
} else {
|
||||
aggBody.field = field.name;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
maxAgg: {
|
||||
max: aggBody,
|
||||
|
@ -44,7 +51,23 @@ const minMaxAgg = field => {
|
|||
};
|
||||
};
|
||||
|
||||
class RangeControl extends Control {
|
||||
export class RangeControl extends Control<RangeFilterManager> {
|
||||
timefilter: TimefilterSetup['timefilter'];
|
||||
abortController: any;
|
||||
min: any;
|
||||
max: any;
|
||||
|
||||
constructor(
|
||||
controlParams: ControlParams,
|
||||
filterManager: RangeFilterManager,
|
||||
useTimeFilter: boolean,
|
||||
SearchSource: SearchSourceClass,
|
||||
deps: InputControlVisDependencies
|
||||
) {
|
||||
super(controlParams, filterManager, useTimeFilter, SearchSource);
|
||||
this.timefilter = deps.data.query.timefilter.timefilter;
|
||||
}
|
||||
|
||||
async fetch() {
|
||||
// Abort any in-progress fetch
|
||||
if (this.abortController) {
|
||||
|
@ -58,14 +81,15 @@ class RangeControl extends Control {
|
|||
}
|
||||
|
||||
const fieldName = this.filterManager.fieldName;
|
||||
|
||||
const aggs = minMaxAgg(indexPattern.fields.getByName(fieldName));
|
||||
const searchSource = createSearchSource(
|
||||
this.SearchSource,
|
||||
null,
|
||||
indexPattern,
|
||||
aggs,
|
||||
this.useTimeFilter
|
||||
this.useTimeFilter,
|
||||
[],
|
||||
this.timefilter
|
||||
);
|
||||
const abortSignal = this.abortController.signal;
|
||||
|
||||
|
@ -102,18 +126,25 @@ class RangeControl extends Control {
|
|||
}
|
||||
}
|
||||
|
||||
export async function rangeControlFactory(controlParams, useTimeFilter, SearchSource) {
|
||||
let indexPattern;
|
||||
try {
|
||||
indexPattern = await npStart.plugins.data.indexPatterns.get(controlParams.indexPattern);
|
||||
} catch (err) {
|
||||
// ignore not found error and return control so it can be displayed in disabled state.
|
||||
}
|
||||
const { filterManager } = npStart.plugins.data.query;
|
||||
export async function rangeControlFactory(
|
||||
controlParams: ControlParams,
|
||||
useTimeFilter: boolean,
|
||||
SearchSource: SearchSourceClass,
|
||||
deps: InputControlVisDependencies
|
||||
): Promise<RangeControl> {
|
||||
const [, { data: dataPluginStart }] = await deps.core.getStartServices();
|
||||
const indexPattern = await dataPluginStart.indexPatterns.get(controlParams.indexPattern);
|
||||
|
||||
return new RangeControl(
|
||||
controlParams,
|
||||
new RangeFilterManager(controlParams.id, controlParams.fieldName, indexPattern, filterManager),
|
||||
new RangeFilterManager(
|
||||
controlParams.id,
|
||||
controlParams.fieldName,
|
||||
indexPattern,
|
||||
deps.data.query.filterManager
|
||||
),
|
||||
useTimeFilter,
|
||||
SearchSource
|
||||
SearchSource,
|
||||
deps
|
||||
);
|
||||
}
|
|
@ -16,21 +16,54 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { $Values } from '@kbn/utility-types';
|
||||
|
||||
export const CONTROL_TYPES = {
|
||||
LIST: 'list',
|
||||
RANGE: 'range',
|
||||
LIST: 'list' as 'list',
|
||||
RANGE: 'range' as 'range',
|
||||
};
|
||||
export type CONTROL_TYPES = $Values<typeof CONTROL_TYPES>;
|
||||
|
||||
export const setControl = (controls, controlIndex, control) => [
|
||||
export interface ControlParamsOptions {
|
||||
decimalPlaces?: number;
|
||||
step?: number;
|
||||
type?: string;
|
||||
multiselect?: boolean;
|
||||
dynamicOptions?: boolean;
|
||||
size?: number;
|
||||
order?: string;
|
||||
}
|
||||
|
||||
export interface ControlParams {
|
||||
id: string;
|
||||
type: CONTROL_TYPES;
|
||||
label: string;
|
||||
fieldName: string;
|
||||
indexPattern: string;
|
||||
parent: string;
|
||||
options: ControlParamsOptions;
|
||||
}
|
||||
|
||||
export const setControl = (
|
||||
controls: ControlParams[],
|
||||
controlIndex: number,
|
||||
control: ControlParams
|
||||
): ControlParams[] => [
|
||||
...controls.slice(0, controlIndex),
|
||||
control,
|
||||
...controls.slice(controlIndex + 1),
|
||||
];
|
||||
|
||||
export const addControl = (controls, control) => [...controls, control];
|
||||
export const addControl = (controls: ControlParams[], control: ControlParams): ControlParams[] => [
|
||||
...controls,
|
||||
control,
|
||||
];
|
||||
|
||||
export const moveControl = (controls, controlIndex, direction) => {
|
||||
export const moveControl = (
|
||||
controls: ControlParams[],
|
||||
controlIndex: number,
|
||||
direction: number
|
||||
): ControlParams[] => {
|
||||
let newIndex;
|
||||
if (direction >= 0) {
|
||||
newIndex = controlIndex + 1;
|
||||
|
@ -54,13 +87,13 @@ export const moveControl = (controls, controlIndex, direction) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const removeControl = (controls, controlIndex) => [
|
||||
export const removeControl = (controls: ControlParams[], controlIndex: number): ControlParams[] => [
|
||||
...controls.slice(0, controlIndex),
|
||||
...controls.slice(controlIndex + 1),
|
||||
];
|
||||
|
||||
export const getDefaultOptions = type => {
|
||||
const defaultOptions = {};
|
||||
export const getDefaultOptions = (type: CONTROL_TYPES): ControlParamsOptions => {
|
||||
const defaultOptions: ControlParamsOptions = {};
|
||||
switch (type) {
|
||||
case CONTROL_TYPES.RANGE:
|
||||
defaultOptions.decimalPlaces = 0;
|
||||
|
@ -77,17 +110,17 @@ export const getDefaultOptions = type => {
|
|||
return defaultOptions;
|
||||
};
|
||||
|
||||
export const newControl = type => ({
|
||||
export const newControl = (type: CONTROL_TYPES): ControlParams => ({
|
||||
id: new Date().getTime().toString(),
|
||||
indexPattern: '',
|
||||
fieldName: '',
|
||||
parent: '',
|
||||
label: '',
|
||||
type: type,
|
||||
type,
|
||||
options: getDefaultOptions(type),
|
||||
});
|
||||
|
||||
export const getTitle = (controlParams, controlIndex) => {
|
||||
export const getTitle = (controlParams: ControlParams, controlIndex: number): string => {
|
||||
let title = `${controlParams.type}: ${controlIndex}`;
|
||||
if (controlParams.label) {
|
||||
title = `${controlParams.label}`;
|
|
@ -17,14 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { PluginInitializerContext } from '../../../../core/public';
|
||||
import { InputControlVisPlugin as Plugin } from './plugin';
|
||||
|
||||
export default function(kibana) {
|
||||
return new kibana.Plugin({
|
||||
uiExports: {
|
||||
visTypes: ['plugins/input_control_vis/register_vis'],
|
||||
interpreter: ['plugins/input_control_vis/input_control_fn'],
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
},
|
||||
});
|
||||
export function plugin(initializerContext: PluginInitializerContext) {
|
||||
return new Plugin(initializerContext);
|
||||
}
|
|
@ -17,14 +17,15 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
import { createInputControlVisFn } from './input_control_fn';
|
||||
|
||||
// eslint-disable-next-line
|
||||
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
|
||||
import { inputControlVis } from './input_control_fn';
|
||||
|
||||
jest.mock('./legacy_imports.ts');
|
||||
|
||||
describe('interpreter/functions#input_control_vis', () => {
|
||||
const fn = functionWrapper(inputControlVis);
|
||||
const fn = functionWrapper(createInputControlVisFn);
|
||||
const visConfig = {
|
||||
controls: [
|
||||
{
|
||||
|
@ -47,8 +48,8 @@ describe('interpreter/functions#input_control_vis', () => {
|
|||
pinFilters: false,
|
||||
};
|
||||
|
||||
it('returns an object with the correct structure', () => {
|
||||
const actual = fn(undefined, { visConfig: JSON.stringify(visConfig) });
|
||||
it('returns an object with the correct structure', async () => {
|
||||
const actual = await fn(null, { visConfig: JSON.stringify(visConfig) });
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -17,10 +17,37 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { functionsRegistry } from 'plugins/interpreter/registries';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const inputControlVis = () => ({
|
||||
import {
|
||||
ExpressionFunction,
|
||||
KibanaDatatable,
|
||||
Render,
|
||||
} from '../../../../plugins/expressions/public';
|
||||
|
||||
const name = 'input_control_vis';
|
||||
|
||||
type Context = KibanaDatatable;
|
||||
|
||||
interface Arguments {
|
||||
visConfig: string;
|
||||
}
|
||||
|
||||
type VisParams = Required<Arguments>;
|
||||
|
||||
interface RenderValue {
|
||||
visType: 'input_control_vis';
|
||||
visConfig: VisParams;
|
||||
}
|
||||
|
||||
type Return = Promise<Render<RenderValue>>;
|
||||
|
||||
export const createInputControlVisFn = (): ExpressionFunction<
|
||||
typeof name,
|
||||
Context,
|
||||
Arguments,
|
||||
Return
|
||||
> => ({
|
||||
name: 'input_control_vis',
|
||||
type: 'render',
|
||||
context: {
|
||||
|
@ -33,9 +60,10 @@ export const inputControlVis = () => ({
|
|||
visConfig: {
|
||||
types: ['string'],
|
||||
default: '"{}"',
|
||||
help: '',
|
||||
},
|
||||
},
|
||||
fn(context, args) {
|
||||
async fn(context, args) {
|
||||
const params = JSON.parse(args.visConfig);
|
||||
return {
|
||||
type: 'render',
|
||||
|
@ -47,5 +75,3 @@ export const inputControlVis = () => ({
|
|||
};
|
||||
},
|
||||
});
|
||||
|
||||
functionsRegistry.register(inputControlVis);
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { createInputControlVisController } from './vis_controller';
|
||||
import { getControlsTab } from './components/editor/controls_tab';
|
||||
import { OptionsTab } from './components/editor/options_tab';
|
||||
import { Status, defaultFeedbackMessage } from '../../visualizations/public';
|
||||
import { InputControlVisDependencies } from './plugin';
|
||||
|
||||
export function createInputControlVisTypeDefinition(deps: InputControlVisDependencies) {
|
||||
const InputControlVisController = createInputControlVisController(deps);
|
||||
const ControlsTab = getControlsTab(deps);
|
||||
|
||||
return {
|
||||
name: 'input_control_vis',
|
||||
title: i18n.translate('inputControl.register.controlsTitle', {
|
||||
defaultMessage: 'Controls',
|
||||
}),
|
||||
icon: 'visControls',
|
||||
description: i18n.translate('inputControl.register.controlsDescription', {
|
||||
defaultMessage: 'Create interactive controls for easy dashboard manipulation.',
|
||||
}),
|
||||
stage: 'experimental',
|
||||
requiresUpdateStatus: [Status.PARAMS, Status.TIME],
|
||||
feedbackMessage: defaultFeedbackMessage,
|
||||
visualization: InputControlVisController,
|
||||
visConfig: {
|
||||
defaults: {
|
||||
controls: [],
|
||||
updateFiltersOnChange: false,
|
||||
useTimeFilter: false,
|
||||
pinFilters: false,
|
||||
},
|
||||
},
|
||||
editor: 'default',
|
||||
editorConfig: {
|
||||
optionTabs: [
|
||||
{
|
||||
name: 'controls',
|
||||
title: i18n.translate('inputControl.register.tabs.controlsTitle', {
|
||||
defaultMessage: 'Controls',
|
||||
}),
|
||||
editor: ControlsTab,
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
title: i18n.translate('inputControl.register.tabs.optionsTitle', {
|
||||
defaultMessage: 'Options',
|
||||
}),
|
||||
editor: OptionsTab,
|
||||
},
|
||||
],
|
||||
},
|
||||
requestHandler: 'none',
|
||||
responseHandler: 'none',
|
||||
};
|
||||
}
|
49
src/legacy/core_plugins/input_control_vis/public/legacy.ts
Normal file
49
src/legacy/core_plugins/input_control_vis/public/legacy.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext } from 'kibana/public';
|
||||
import { npSetup, npStart } from 'ui/new_platform';
|
||||
|
||||
import { plugin } from '.';
|
||||
|
||||
import {
|
||||
InputControlVisPluginSetupDependencies,
|
||||
InputControlVisPluginStartDependencies,
|
||||
} from './plugin';
|
||||
import {
|
||||
setup as visualizationsSetup,
|
||||
start as visualizationsStart,
|
||||
} from '../../visualizations/public/np_ready/public/legacy';
|
||||
|
||||
const setupPlugins: Readonly<InputControlVisPluginSetupDependencies> = {
|
||||
expressions: npSetup.plugins.expressions,
|
||||
data: npSetup.plugins.data,
|
||||
visualizations: visualizationsSetup,
|
||||
};
|
||||
|
||||
const startPlugins: Readonly<InputControlVisPluginStartDependencies> = {
|
||||
expressions: npStart.plugins.expressions,
|
||||
data: npStart.plugins.data,
|
||||
visualizations: visualizationsStart,
|
||||
};
|
||||
|
||||
const pluginInstance = plugin({} as PluginInitializerContext);
|
||||
|
||||
export const setup = pluginInstance.setup(npSetup.core, setupPlugins);
|
||||
export const start = pluginInstance.start(npStart.core, startPlugins);
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SearchSource as SearchSourceClass } from 'ui/courier';
|
||||
import { Class } from '@kbn/utility-types';
|
||||
|
||||
export { Vis, VisParams } from 'ui/vis';
|
||||
export { VisOptionsProps } from 'ui/vis/editors/default';
|
||||
export { ValidatedDualRange } from 'ui/validated_range';
|
||||
export { SearchSourceFields } from 'ui/courier/types';
|
||||
|
||||
export type SearchSource = Class<SearchSourceClass>;
|
||||
export const SearchSource = SearchSourceClass;
|
|
@ -23,11 +23,11 @@ import { CONTROL_TYPES, newControl } from '../editor_utils';
|
|||
|
||||
test('creates lineage map', () => {
|
||||
const control1 = newControl(CONTROL_TYPES.LIST);
|
||||
control1.id = 1;
|
||||
control1.id = '1';
|
||||
const control2 = newControl(CONTROL_TYPES.LIST);
|
||||
control2.id = 2;
|
||||
control2.id = '2';
|
||||
const control3 = newControl(CONTROL_TYPES.LIST);
|
||||
control3.id = 3;
|
||||
control3.id = '3';
|
||||
|
||||
control2.parent = control1.id;
|
||||
control3.parent = control2.id;
|
||||
|
@ -40,9 +40,9 @@ test('creates lineage map', () => {
|
|||
|
||||
test('safely handles circular graph', () => {
|
||||
const control1 = newControl(CONTROL_TYPES.LIST);
|
||||
control1.id = 1;
|
||||
control1.id = '1';
|
||||
const control2 = newControl(CONTROL_TYPES.LIST);
|
||||
control2.id = 2;
|
||||
control2.id = '2';
|
||||
|
||||
control1.parent = control2.id;
|
||||
control2.parent = control1.id;
|
|
@ -18,18 +18,19 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { ControlParams } from '../editor_utils';
|
||||
|
||||
export function getLineageMap(controlParamsList) {
|
||||
function getControlParamsById(controlId) {
|
||||
export function getLineageMap(controlParamsList: ControlParams[]) {
|
||||
function getControlParamsById(controlId: string) {
|
||||
return controlParamsList.find(controlParams => {
|
||||
return controlParams.id === controlId;
|
||||
});
|
||||
}
|
||||
|
||||
const lineageMap = new Map();
|
||||
const lineageMap = new Map<string, string[]>();
|
||||
controlParamsList.forEach(rootControlParams => {
|
||||
const lineage = [rootControlParams.id];
|
||||
const getLineage = controlParams => {
|
||||
const getLineage = (controlParams: ControlParams) => {
|
||||
if (
|
||||
_.has(controlParams, 'parent') &&
|
||||
controlParams.parent !== '' &&
|
||||
|
@ -37,7 +38,10 @@ export function getLineageMap(controlParamsList) {
|
|||
) {
|
||||
lineage.push(controlParams.parent);
|
||||
const parent = getControlParamsById(controlParams.parent);
|
||||
getLineage(parent);
|
||||
|
||||
if (parent) {
|
||||
getLineage(parent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -22,7 +22,7 @@ import { getLineageMap } from './lineage_map';
|
|||
import { getParentCandidates } from './parent_candidates';
|
||||
import { CONTROL_TYPES, newControl } from '../editor_utils';
|
||||
|
||||
function createControlParams(id) {
|
||||
function createControlParams(id: any) {
|
||||
const controlParams = newControl(CONTROL_TYPES.LIST);
|
||||
controlParams.id = id;
|
||||
controlParams.indexPattern = 'indexPatternId';
|
|
@ -17,9 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { getTitle } from '../editor_utils';
|
||||
import { getTitle, ControlParams } from '../editor_utils';
|
||||
|
||||
export function getParentCandidates(controlParamsList, controlId, lineageMap) {
|
||||
export function getParentCandidates(
|
||||
controlParamsList: ControlParams[],
|
||||
controlId: string,
|
||||
lineageMap: Map<string, string[]>
|
||||
) {
|
||||
return controlParamsList
|
||||
.filter(controlParams => {
|
||||
// Ignore controls that do not have index pattern and field set
|
||||
|
@ -28,7 +32,7 @@ export function getParentCandidates(controlParamsList, controlId, lineageMap) {
|
|||
}
|
||||
// Ignore controls that would create a circular graph
|
||||
const lineage = lineageMap.get(controlParams.id);
|
||||
if (lineage.includes(controlId)) {
|
||||
if (lineage?.includes(controlId)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
70
src/legacy/core_plugins/input_control_vis/public/plugin.ts
Normal file
70
src/legacy/core_plugins/input_control_vis/public/plugin.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public';
|
||||
|
||||
import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public';
|
||||
import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public';
|
||||
import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public';
|
||||
import { createInputControlVisFn } from './input_control_fn';
|
||||
import { createInputControlVisTypeDefinition } from './input_control_vis_type';
|
||||
|
||||
type InputControlVisCoreSetup = CoreSetup<InputControlVisPluginStartDependencies>;
|
||||
|
||||
export interface InputControlVisDependencies {
|
||||
core: InputControlVisCoreSetup;
|
||||
data: DataPublicPluginSetup;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InputControlVisPluginSetupDependencies {
|
||||
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;
|
||||
visualizations: VisualizationsSetup;
|
||||
data: DataPublicPluginSetup;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InputControlVisPluginStartDependencies {
|
||||
expressions: ReturnType<ExpressionsPublicPlugin['start']>;
|
||||
visualizations: VisualizationsStart;
|
||||
data: DataPublicPluginStart;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class InputControlVisPlugin implements Plugin<Promise<void>, void> {
|
||||
constructor(public initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public async setup(
|
||||
core: InputControlVisCoreSetup,
|
||||
{ expressions, visualizations, data }: InputControlVisPluginSetupDependencies
|
||||
) {
|
||||
const visualizationDependencies: Readonly<InputControlVisDependencies> = {
|
||||
core,
|
||||
data,
|
||||
};
|
||||
|
||||
expressions.registerFunction(createInputControlVisFn);
|
||||
visualizations.types.createBaseVisualization(
|
||||
createInputControlVisTypeDefinition(visualizationDependencies)
|
||||
);
|
||||
}
|
||||
|
||||
public start(core: CoreStart, deps: InputControlVisPluginStartDependencies) {
|
||||
// nothing to do here
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { VisController } from './vis_controller';
|
||||
import { ControlsTab } from './components/editor/controls_tab';
|
||||
import { OptionsTab } from './components/editor/options_tab';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { setup as visualizations } from '../../visualizations/public/np_ready/public/legacy';
|
||||
import { Status, defaultFeedbackMessage } from '../../visualizations/public';
|
||||
|
||||
export const inputControlVisDefinition = {
|
||||
name: 'input_control_vis',
|
||||
title: i18n.translate('inputControl.register.controlsTitle', {
|
||||
defaultMessage: 'Controls',
|
||||
}),
|
||||
icon: 'visControls',
|
||||
description: i18n.translate('inputControl.register.controlsDescription', {
|
||||
defaultMessage: 'Create interactive controls for easy dashboard manipulation.',
|
||||
}),
|
||||
stage: 'experimental',
|
||||
requiresUpdateStatus: [Status.PARAMS, Status.TIME],
|
||||
feedbackMessage: defaultFeedbackMessage,
|
||||
visualization: VisController,
|
||||
visConfig: {
|
||||
defaults: {
|
||||
controls: [],
|
||||
updateFiltersOnChange: false,
|
||||
useTimeFilter: false,
|
||||
pinFilters: false,
|
||||
},
|
||||
},
|
||||
editor: 'default',
|
||||
editorConfig: {
|
||||
optionTabs: [
|
||||
{
|
||||
name: 'controls',
|
||||
title: i18n.translate('inputControl.register.tabs.controlsTitle', {
|
||||
defaultMessage: 'Controls',
|
||||
}),
|
||||
editor: ControlsTab,
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
title: i18n.translate('inputControl.register.tabs.optionsTitle', {
|
||||
defaultMessage: 'Options',
|
||||
}),
|
||||
editor: OptionsTab,
|
||||
},
|
||||
],
|
||||
},
|
||||
requestHandler: 'none',
|
||||
responseHandler: 'none',
|
||||
};
|
||||
|
||||
// register the provider with the visTypes registry
|
||||
visualizations.types.createBaseVisualization(inputControlVisDefinition);
|
|
@ -1,207 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { InputControlVis } from './components/vis/input_control_vis';
|
||||
import { controlFactory } from './control/control_factory';
|
||||
import { getLineageMap } from './lineage';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import { SearchSource } from '../../../ui/public/courier/search_source/search_source';
|
||||
|
||||
class VisController {
|
||||
constructor(el, vis) {
|
||||
this.el = el;
|
||||
this.vis = vis;
|
||||
this.controls = [];
|
||||
|
||||
this.queryBarUpdateHandler = this.updateControlsFromKbn.bind(this);
|
||||
|
||||
this.filterManager = npStart.plugins.data.query.filterManager;
|
||||
this.updateSubsciption = this.filterManager.getUpdates$().subscribe(this.queryBarUpdateHandler);
|
||||
}
|
||||
|
||||
async render(visData, visParams, status) {
|
||||
if (status.params || (visParams.useTimeFilter && status.time)) {
|
||||
this.visParams = visParams;
|
||||
this.controls = [];
|
||||
this.controls = await this.initControls();
|
||||
this.drawVis();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.updateSubsciption.unsubscribe();
|
||||
unmountComponentAtNode(this.el);
|
||||
this.controls.forEach(control => control.destroy());
|
||||
}
|
||||
|
||||
drawVis = () => {
|
||||
render(
|
||||
<I18nContext>
|
||||
<InputControlVis
|
||||
updateFiltersOnChange={this.visParams.updateFiltersOnChange}
|
||||
controls={this.controls}
|
||||
stageFilter={this.stageFilter}
|
||||
submitFilters={this.submitFilters}
|
||||
resetControls={this.updateControlsFromKbn}
|
||||
clearControls={this.clearControls}
|
||||
hasChanges={this.hasChanges}
|
||||
hasValues={this.hasValues}
|
||||
refreshControl={this.refreshControl}
|
||||
/>
|
||||
</I18nContext>,
|
||||
this.el
|
||||
);
|
||||
};
|
||||
|
||||
async initControls() {
|
||||
const controlParamsList = this.visParams.controls.filter(controlParams => {
|
||||
// ignore controls that do not have indexPattern or field
|
||||
return controlParams.indexPattern && controlParams.fieldName;
|
||||
});
|
||||
|
||||
const controlFactoryPromises = controlParamsList.map(controlParams => {
|
||||
const factory = controlFactory(controlParams);
|
||||
return factory(controlParams, this.visParams.useTimeFilter, SearchSource);
|
||||
});
|
||||
const controls = await Promise.all(controlFactoryPromises);
|
||||
|
||||
const getControl = id => {
|
||||
return controls.find(control => {
|
||||
return id === control.id;
|
||||
});
|
||||
};
|
||||
|
||||
const controlInitPromises = [];
|
||||
getLineageMap(controlParamsList).forEach((lineage, controlId) => {
|
||||
// first lineage item is the control. remove it
|
||||
lineage.shift();
|
||||
const ancestors = [];
|
||||
lineage.forEach(ancestorId => {
|
||||
ancestors.push(getControl(ancestorId));
|
||||
});
|
||||
const control = getControl(controlId);
|
||||
control.setAncestors(ancestors);
|
||||
controlInitPromises.push(control.fetch());
|
||||
});
|
||||
|
||||
await Promise.all(controlInitPromises);
|
||||
return controls;
|
||||
}
|
||||
|
||||
stageFilter = async (controlIndex, newValue) => {
|
||||
this.controls[controlIndex].set(newValue);
|
||||
if (this.visParams.updateFiltersOnChange) {
|
||||
// submit filters on each control change
|
||||
this.submitFilters();
|
||||
} else {
|
||||
// Do not submit filters, just update vis so controls are updated with latest value
|
||||
await this.updateNestedControls();
|
||||
this.drawVis();
|
||||
}
|
||||
};
|
||||
|
||||
submitFilters = () => {
|
||||
const stagedControls = this.controls.filter(control => {
|
||||
return control.hasChanged();
|
||||
});
|
||||
|
||||
const newFilters = stagedControls
|
||||
.filter(control => {
|
||||
return control.hasKbnFilter();
|
||||
})
|
||||
.map(control => {
|
||||
return control.getKbnFilter();
|
||||
});
|
||||
|
||||
stagedControls.forEach(control => {
|
||||
// to avoid duplicate filters, remove any old filters for control
|
||||
control.filterManager.findFilters().forEach(existingFilter => {
|
||||
this.filterManager.removeFilter(existingFilter);
|
||||
});
|
||||
});
|
||||
|
||||
// Clean up filter pills for nested controls that are now disabled because ancestors are not set.
|
||||
// This has to be done after looking up the staged controls because otherwise removing a filter
|
||||
// will re-sync the controls of all other filters.
|
||||
this.controls.map(control => {
|
||||
if (control.hasAncestors() && control.hasUnsetAncestor()) {
|
||||
control.filterManager.findFilters().forEach(existingFilter => {
|
||||
this.filterManager.removeFilter(existingFilter);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.filterManager.addFilters(newFilters, this.visParams.pinFilters);
|
||||
};
|
||||
|
||||
clearControls = async () => {
|
||||
this.controls.forEach(control => {
|
||||
control.clear();
|
||||
});
|
||||
await this.updateNestedControls();
|
||||
this.drawVis();
|
||||
};
|
||||
|
||||
updateControlsFromKbn = async () => {
|
||||
this.controls.forEach(control => {
|
||||
control.reset();
|
||||
});
|
||||
await this.updateNestedControls();
|
||||
this.drawVis();
|
||||
};
|
||||
|
||||
async updateNestedControls() {
|
||||
const fetchPromises = this.controls.map(async control => {
|
||||
if (control.hasAncestors()) {
|
||||
await control.fetch();
|
||||
}
|
||||
});
|
||||
return await Promise.all(fetchPromises);
|
||||
}
|
||||
|
||||
hasChanges = () => {
|
||||
return this.controls
|
||||
.map(control => {
|
||||
return control.hasChanged();
|
||||
})
|
||||
.reduce((a, b) => {
|
||||
return a || b;
|
||||
});
|
||||
};
|
||||
|
||||
hasValues = () => {
|
||||
return this.controls
|
||||
.map(control => {
|
||||
return control.hasValue();
|
||||
})
|
||||
.reduce((a, b) => {
|
||||
return a || b;
|
||||
});
|
||||
};
|
||||
|
||||
refreshControl = async (controlIndex, query) => {
|
||||
await this.controls[controlIndex].fetch(query);
|
||||
this.drawVis();
|
||||
};
|
||||
}
|
||||
|
||||
export { VisController };
|
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
|
||||
import { I18nStart } from 'kibana/public';
|
||||
import { Vis, VisParams, SearchSource } from './legacy_imports';
|
||||
|
||||
import { InputControlVis } from './components/vis/input_control_vis';
|
||||
import { getControlFactory } from './control/control_factory';
|
||||
import { getLineageMap } from './lineage';
|
||||
import { ControlParams } from './editor_utils';
|
||||
import { RangeControl } from './control/range_control_factory';
|
||||
import { ListControl } from './control/list_control_factory';
|
||||
import { InputControlVisDependencies } from './plugin';
|
||||
import { FilterManager, esFilters } from '../../../../plugins/data/public';
|
||||
|
||||
export const createInputControlVisController = (deps: InputControlVisDependencies) => {
|
||||
return class InputControlVisController {
|
||||
private I18nContext?: I18nStart['Context'];
|
||||
|
||||
controls: Array<RangeControl | ListControl>;
|
||||
queryBarUpdateHandler: () => void;
|
||||
filterManager: FilterManager;
|
||||
updateSubsciption: any;
|
||||
visParams?: VisParams;
|
||||
|
||||
constructor(public el: Element, public vis: Vis) {
|
||||
this.controls = [];
|
||||
|
||||
this.queryBarUpdateHandler = this.updateControlsFromKbn.bind(this);
|
||||
|
||||
this.filterManager = deps.data.query.filterManager;
|
||||
this.updateSubsciption = this.filterManager
|
||||
.getUpdates$()
|
||||
.subscribe(this.queryBarUpdateHandler);
|
||||
}
|
||||
|
||||
async render(visData: any, visParams: VisParams, status: any) {
|
||||
if (status.params || (visParams.useTimeFilter && status.time)) {
|
||||
this.visParams = visParams;
|
||||
this.controls = [];
|
||||
this.controls = await this.initControls();
|
||||
const [{ i18n }] = await deps.core.getStartServices();
|
||||
this.I18nContext = i18n.Context;
|
||||
this.drawVis();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.updateSubsciption.unsubscribe();
|
||||
unmountComponentAtNode(this.el);
|
||||
this.controls.forEach(control => control.destroy());
|
||||
}
|
||||
|
||||
drawVis = () => {
|
||||
if (!this.I18nContext) {
|
||||
throw new Error('no i18n context found');
|
||||
}
|
||||
|
||||
render(
|
||||
<this.I18nContext>
|
||||
<InputControlVis
|
||||
updateFiltersOnChange={this.visParams?.updateFiltersOnChange}
|
||||
controls={this.controls}
|
||||
stageFilter={this.stageFilter}
|
||||
submitFilters={this.submitFilters}
|
||||
resetControls={this.updateControlsFromKbn}
|
||||
clearControls={this.clearControls}
|
||||
hasChanges={this.hasChanges}
|
||||
hasValues={this.hasValues}
|
||||
refreshControl={this.refreshControl}
|
||||
/>
|
||||
</this.I18nContext>,
|
||||
this.el
|
||||
);
|
||||
};
|
||||
|
||||
async initControls() {
|
||||
const controlParamsList = (this.visParams?.controls as ControlParams[])?.filter(
|
||||
controlParams => {
|
||||
// ignore controls that do not have indexPattern or field
|
||||
return controlParams.indexPattern && controlParams.fieldName;
|
||||
}
|
||||
);
|
||||
|
||||
const controlFactoryPromises = controlParamsList.map(controlParams => {
|
||||
const factory = getControlFactory(controlParams);
|
||||
return factory(controlParams, this.visParams?.useTimeFilter, SearchSource, deps);
|
||||
});
|
||||
const controls = await Promise.all<RangeControl | ListControl>(controlFactoryPromises);
|
||||
|
||||
const getControl = (controlId: string) => {
|
||||
return controls.find(({ id }) => id === controlId);
|
||||
};
|
||||
|
||||
const controlInitPromises: Array<Promise<void>> = [];
|
||||
getLineageMap(controlParamsList).forEach((lineage, controlId) => {
|
||||
// first lineage item is the control. remove it
|
||||
lineage.shift();
|
||||
const ancestors: Array<RangeControl | ListControl> = [];
|
||||
lineage.forEach(ancestorId => {
|
||||
const control = getControl(ancestorId);
|
||||
|
||||
if (control) {
|
||||
ancestors.push(control);
|
||||
}
|
||||
});
|
||||
const control = getControl(controlId);
|
||||
|
||||
if (control) {
|
||||
control.setAncestors(ancestors);
|
||||
controlInitPromises.push(control.fetch());
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(controlInitPromises);
|
||||
return controls;
|
||||
}
|
||||
|
||||
stageFilter = async (controlIndex: number, newValue: any) => {
|
||||
this.controls[controlIndex].set(newValue);
|
||||
if (this.visParams?.updateFiltersOnChange) {
|
||||
// submit filters on each control change
|
||||
this.submitFilters();
|
||||
} else {
|
||||
// Do not submit filters, just update vis so controls are updated with latest value
|
||||
await this.updateNestedControls();
|
||||
this.drawVis();
|
||||
}
|
||||
};
|
||||
|
||||
submitFilters = () => {
|
||||
const stagedControls = this.controls.filter(control => {
|
||||
return control.hasChanged();
|
||||
});
|
||||
|
||||
const newFilters = stagedControls
|
||||
.map(control => control.getKbnFilter())
|
||||
.filter((filter): filter is esFilters.Filter => {
|
||||
return filter !== null;
|
||||
});
|
||||
|
||||
stagedControls.forEach(control => {
|
||||
// to avoid duplicate filters, remove any old filters for control
|
||||
control.filterManager.findFilters().forEach(existingFilter => {
|
||||
this.filterManager.removeFilter(existingFilter);
|
||||
});
|
||||
});
|
||||
|
||||
// Clean up filter pills for nested controls that are now disabled because ancestors are not set.
|
||||
// This has to be done after looking up the staged controls because otherwise removing a filter
|
||||
// will re-sync the controls of all other filters.
|
||||
this.controls.map(control => {
|
||||
if (control.hasAncestors() && control.hasUnsetAncestor()) {
|
||||
control.filterManager.findFilters().forEach(existingFilter => {
|
||||
this.filterManager.removeFilter(existingFilter);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.filterManager.addFilters(newFilters, this.visParams?.pinFilters);
|
||||
};
|
||||
|
||||
clearControls = async () => {
|
||||
this.controls.forEach(control => {
|
||||
control.clear();
|
||||
});
|
||||
await this.updateNestedControls();
|
||||
this.drawVis();
|
||||
};
|
||||
|
||||
updateControlsFromKbn = async () => {
|
||||
this.controls.forEach(control => {
|
||||
control.reset();
|
||||
});
|
||||
await this.updateNestedControls();
|
||||
this.drawVis();
|
||||
};
|
||||
|
||||
async updateNestedControls() {
|
||||
const fetchPromises = this.controls.map(async control => {
|
||||
if (control.hasAncestors()) {
|
||||
await control.fetch();
|
||||
}
|
||||
});
|
||||
return await Promise.all(fetchPromises);
|
||||
}
|
||||
|
||||
hasChanges = () => {
|
||||
return this.controls.map(control => control.hasChanged()).some(control => control);
|
||||
};
|
||||
|
||||
hasValues = () => {
|
||||
return this.controls
|
||||
.map(control => {
|
||||
return control.hasValue();
|
||||
})
|
||||
.reduce((a, b) => {
|
||||
return a || b;
|
||||
});
|
||||
};
|
||||
|
||||
refreshControl = async (controlIndex: number, query: any) => {
|
||||
await this.controls[controlIndex].fetch(query);
|
||||
this.drawVis();
|
||||
};
|
||||
};
|
||||
};
|
|
@ -19,9 +19,9 @@
|
|||
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { NameList } from 'elasticsearch';
|
||||
import { esFilters, Query } from '../../../../../plugins/data/public';
|
||||
import { IndexPattern } from '../../../../core_plugins/data/public/index_patterns';
|
||||
import { esFilters, Query, IndexPattern } from '../../../../../plugins/data/public';
|
||||
|
||||
export type EsQuerySearchAfter = [string | number, string | number];
|
||||
|
||||
|
@ -47,6 +46,8 @@ export interface SearchSourceFields {
|
|||
fields?: NameList;
|
||||
index?: IndexPattern;
|
||||
searchAfter?: EsQuerySearchAfter;
|
||||
timeout?: string;
|
||||
terminate_after?: number;
|
||||
}
|
||||
|
||||
export interface SearchSourceOptions {
|
||||
|
|
|
@ -42,7 +42,11 @@ export const getPhrasesFilterField = (filter: PhrasesFilter) => {
|
|||
|
||||
// Creates a filter where the given field matches one or more of the given values
|
||||
// params should be an array of values
|
||||
export const buildPhrasesFilter = (field: IFieldType, params: any, indexPattern: IIndexPattern) => {
|
||||
export const buildPhrasesFilter = (
|
||||
field: IFieldType,
|
||||
params: any[],
|
||||
indexPattern: IIndexPattern
|
||||
) => {
|
||||
const index = indexPattern.id;
|
||||
const type = FILTERS.PHRASES;
|
||||
const key = field.name;
|
||||
|
|
|
@ -18,8 +18,9 @@
|
|||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { IIndexPattern } from '../..';
|
||||
|
||||
export function getFromSavedObject(savedObject: any) {
|
||||
export function getFromSavedObject(savedObject: any): IIndexPattern | undefined {
|
||||
if (get(savedObject, 'attributes.fields') === undefined) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -18,10 +18,10 @@
|
|||
*/
|
||||
|
||||
import dateMath from '@elastic/datemath';
|
||||
import { TimeRange } from '../../../common';
|
||||
import { IIndexPattern } from '../..';
|
||||
import { TimeRange, IFieldType } from '../../../common';
|
||||
|
||||
// TODO: remove this
|
||||
import { IndexPattern, Field } from '../../../../../legacy/core_plugins/data/public';
|
||||
import { esFilters } from '../../../common';
|
||||
|
||||
interface CalculateBoundsOptions {
|
||||
|
@ -36,7 +36,7 @@ export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOp
|
|||
}
|
||||
|
||||
export function getTime(
|
||||
indexPattern: IndexPattern | undefined,
|
||||
indexPattern: IIndexPattern | undefined,
|
||||
timeRange: TimeRange,
|
||||
forceNow?: Date
|
||||
) {
|
||||
|
@ -45,7 +45,7 @@ export function getTime(
|
|||
return;
|
||||
}
|
||||
|
||||
const timefield: Field | undefined = indexPattern.fields.find(
|
||||
const timefield: IFieldType | undefined = indexPattern.fields.find(
|
||||
field => field.name === indexPattern.timeFieldName
|
||||
);
|
||||
|
||||
|
|
|
@ -20,18 +20,21 @@
|
|||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { EuiComboBox } from '@elastic/eui';
|
||||
import { Required } from '@kbn/utility-types';
|
||||
import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui';
|
||||
|
||||
import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public';
|
||||
import { getTitle } from '../../index_patterns/lib';
|
||||
|
||||
export interface IndexPatternSelectProps {
|
||||
onChange: (opt: any) => void;
|
||||
export type IndexPatternSelectProps = Required<
|
||||
Omit<EuiComboBoxProps<any>, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions'>,
|
||||
'onChange' | 'placeholder'
|
||||
> & {
|
||||
indexPatternId: string;
|
||||
placeholder: string;
|
||||
fieldTypes: string[];
|
||||
onNoIndexPatterns: () => void;
|
||||
fieldTypes?: string[];
|
||||
onNoIndexPatterns?: () => void;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
}
|
||||
};
|
||||
|
||||
interface IndexPatternSelectState {
|
||||
isLoading: boolean;
|
||||
|
@ -136,7 +139,7 @@ export class IndexPatternSelect extends Component<IndexPatternSelectProps> {
|
|||
try {
|
||||
const indexPatternFields = JSON.parse(savedObject.attributes.fields as any);
|
||||
return indexPatternFields.some((field: any) => {
|
||||
return fieldTypes.includes(field.type);
|
||||
return fieldTypes?.includes(field.type);
|
||||
});
|
||||
} catch (err) {
|
||||
// Unable to parse fields JSON, invalid index pattern
|
||||
|
@ -196,6 +199,7 @@ export class IndexPatternSelect extends Component<IndexPatternSelectProps> {
|
|||
|
||||
return (
|
||||
<EuiComboBox
|
||||
{...rest}
|
||||
placeholder={placeholder}
|
||||
singleSelection={true}
|
||||
isLoading={this.state.isLoading}
|
||||
|
@ -203,7 +207,6 @@ export class IndexPatternSelect extends Component<IndexPatternSelectProps> {
|
|||
options={this.state.options}
|
||||
selectedOptions={this.state.selectedIndexPattern ? [this.state.selectedIndexPattern] : []}
|
||||
onChange={this.onChange}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -92,7 +92,6 @@ describe('getLinearGradient', () => {
|
|||
'rgb(32,112,180)',
|
||||
'rgb(7,47,107)',
|
||||
];
|
||||
|
||||
expect(getLinearGradient(colorRamp)).toBe(
|
||||
'linear-gradient(to right, rgb(247,250,255) 0%, rgb(221,234,247) 14%, rgb(197,218,238) 28%, rgb(157,201,224) 42%, rgb(106,173,213) 57%, rgb(65,145,197) 71%, rgb(32,112,180) 85%, rgb(7,47,107) 100%)'
|
||||
);
|
||||
|
|
|
@ -27,7 +27,6 @@ describe('styleSvg', () => {
|
|||
const unstyledSvgString =
|
||||
'<svg version="1.1" width="11px" height="11px" viewBox="0 0 11 11"><path/></svg>';
|
||||
const styledSvg = await styleSvg(unstyledSvgString, 'red');
|
||||
|
||||
expect(styledSvg.split('\n')[1]).toBe(
|
||||
'<svg version="1.1" width="11px" height="11px" viewBox="0 0 11 11" style="fill:red;">'
|
||||
);
|
||||
|
@ -37,7 +36,6 @@ describe('styleSvg', () => {
|
|||
const unstyledSvgString =
|
||||
'<svg version="1.1" width="11px" height="11px" viewBox="0 0 11 11"><path/></svg>';
|
||||
const styledSvg = await styleSvg(unstyledSvgString, 'red', 'white');
|
||||
|
||||
expect(styledSvg.split('\n')[1]).toBe(
|
||||
'<svg version="1.1" width="11px" height="11px" viewBox="0 0 11 11" style="fill:red;stroke:white;">'
|
||||
);
|
||||
|
@ -47,7 +45,6 @@ describe('styleSvg', () => {
|
|||
const unstyledSvgString =
|
||||
'<svg version="1.1" width="11px" height="11px" viewBox="0 0 11 11"><path/></svg>';
|
||||
const styledSvg = await styleSvg(unstyledSvgString, 'red', 'white', '2px');
|
||||
|
||||
expect(styledSvg.split('\n')[1]).toBe(
|
||||
'<svg version="1.1" width="11px" height="11px" viewBox="0 0 11 11" style="fill:red;stroke:white;stroke-width:2px;">'
|
||||
);
|
||||
|
|
|
@ -81,7 +81,6 @@ export class VectorStyle extends AbstractStyle {
|
|||
this._descriptor.properties[VECTOR_STYLES.ICON_SIZE],
|
||||
VECTOR_STYLES.ICON_SIZE
|
||||
);
|
||||
|
||||
this._iconOrientationProperty = this._makeOrientationProperty(
|
||||
this._descriptor.properties[VECTOR_STYLES.ICON_ORIENTATION],
|
||||
VECTOR_STYLES.ICON_ORIENTATION
|
||||
|
|
|
@ -42,7 +42,6 @@ export function getEMSClient() {
|
|||
false
|
||||
);
|
||||
const proxyPath = proxyElasticMapsServiceInMaps ? relativeToAbsolute('..') : '';
|
||||
|
||||
const manifestServiceUrl = proxyElasticMapsServiceInMaps
|
||||
? relativeToAbsolute(`${GIS_API_RELATIVE}/${EMS_CATALOGUE_PATH}`)
|
||||
: chrome.getInjected('emsManifestServiceUrl');
|
||||
|
|
|
@ -29217,10 +29217,10 @@ utila@^0.4.0, utila@~0.4:
|
|||
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
|
||||
integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
|
||||
|
||||
utility-types@^3.7.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.7.0.tgz#51f1c29fa35d4267488345706efcf3f68f2b1933"
|
||||
integrity sha512-mqRJXN7dEArK/NZNJUubjr9kbFFVZcmF/JHDc9jt5O/aYXUVmopHYujDMhLmLil1Bxo2+khe6KAIVvDH9Yc4VA==
|
||||
utility-types@^3.10.0:
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
|
||||
integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
|
||||
|
||||
utils-copy-error@^1.0.0:
|
||||
version "1.0.1"
|
||||
|
|
Loading…
Reference in a new issue