From 72c5c5cb22b10096284c173bd49a095b2286b04f Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Wed, 18 Dec 2019 16:25:35 -0600 Subject: [PATCH] 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 --- packages/kbn-utility-types/index.ts | 1 + packages/kbn-utility-types/package.json | 2 +- .../mode/script_highlight_rules.js | 1 - .../core_plugins/input_control_vis/index.ts | 44 ++++ ....js.snap => input_control_fn.test.ts.snap} | 0 ...est.js.snap => controls_tab.test.tsx.snap} | 48 ++++ ...snap => list_control_editor.test.tsx.snap} | 6 + ...test.js.snap => options_tab.test.tsx.snap} | 1 + ...nap => range_control_editor.test.tsx.snap} | 1 + .../editor/__tests__/get_deps_mock.tsx | 76 ++++++ ...tern_mock.js => get_index_pattern_mock.ts} | 9 +- ...rns_mock.js => get_index_patterns_mock.ts} | 0 .../__tests__/get_search_service_mock.ts | 46 ++++ .../editor/__tests__/update_component.ts | 31 +++ .../{control_editor.js => control_editor.tsx} | 83 ++++--- ...rols_tab.test.js => controls_tab.test.tsx} | 49 ++-- .../{controls_tab.js => controls_tab.tsx} | 102 +++++--- .../{field_select.js => field_select.tsx} | 75 +++--- ...w.js => index_pattern_select_form_row.tsx} | 25 +- ...r.test.js => list_control_editor.test.tsx} | 128 +++++----- ...trol_editor.js => list_control_editor.tsx} | 140 +++++++---- ...tions_tab.test.js => options_tab.test.tsx} | 8 +- .../{options_tab.js => options_tab.tsx} | 38 +-- .../components/editor/range_control_editor.js | 102 -------- ....test.js => range_control_editor.test.tsx} | 74 +++--- .../editor/range_control_editor.tsx | 139 +++++++++++ ...ow.test.js.snap => form_row.test.tsx.snap} | 1 - ...s.snap => input_control_vis.test.tsx.snap} | 3 - ...est.js.snap => list_control.test.tsx.snap} | 1 + ...st.js.snap => range_control.test.tsx.snap} | 1 - .../{form_row.test.js => form_row.test.tsx} | 0 .../vis/{form_row.js => form_row.tsx} | 25 +- ...vis.test.js => input_control_vis.test.tsx} | 45 ++-- ...t_control_vis.js => input_control_vis.tsx} | 108 +++++---- ..._control.test.js => list_control.test.tsx} | 7 +- .../vis/{list_control.js => list_control.tsx} | 79 +++--- ...control.test.js => range_control.test.tsx} | 11 +- .../{range_control.js => range_control.tsx} | 44 ++-- .../{control.test.js => control.test.ts} | 63 +++-- .../public/control/{control.js => control.ts} | 71 +++--- ...{control_factory.js => control_factory.ts} | 7 +- ...arch_source.js => create_search_source.ts} | 22 +- ...manager.test.js => filter_manager.test.ts} | 45 ++-- .../{filter_manager.js => filter_manager.ts} | 44 ++-- ....test.js => phrase_filter_manager.test.ts} | 70 +++--- ...er_manager.js => phrase_filter_manager.ts} | 59 ++--- ...r.test.js => range_filter_manager.test.ts} | 68 ++++-- ...ter_manager.js => range_filter_manager.ts} | 25 +- ...y.test.js => list_control_factory.test.ts} | 133 ++++------- ...rol_factory.js => list_control_factory.ts} | 109 ++++++--- ....test.js => range_control_factory.test.ts} | 85 +++---- ...ol_factory.js => range_control_factory.ts} | 79 ++++-- .../{editor_utils.js => editor_utils.ts} | 55 ++++- .../{index.js => public/index.ts} | 13 +- ...ol_fn.test.js => input_control_fn.test.ts} | 11 +- ...nput_control_fn.js => input_control_fn.ts} | 36 ++- .../public/input_control_vis_type.ts | 75 ++++++ .../input_control_vis/public/legacy.ts | 49 ++++ .../public/legacy_imports.ts | 29 +++ .../public/lineage/{index.js => index.ts} | 0 ...ineage_map.test.js => lineage_map.test.ts} | 10 +- .../{lineage_map.js => lineage_map.ts} | 14 +- ...ates.test.js => parent_candidates.test.ts} | 2 +- ...ent_candidates.js => parent_candidates.ts} | 10 +- .../input_control_vis/public/plugin.ts | 70 ++++++ .../input_control_vis/public/register_vis.js | 72 ------ .../public/vis_controller.js | 207 ---------------- .../public/vis_controller.tsx | 226 ++++++++++++++++++ .../dashboard/listing/dashboard_listing.js | 2 +- .../ui/public/courier/search_source/types.ts | 5 +- .../common/es_query/filters/phrases_filter.ts | 6 +- .../lib/get_from_saved_object.ts | 3 +- .../data/public/query/timefilter/get_time.ts | 8 +- .../index_pattern_select.tsx | 21 +- .../public/layers/styles/color_utils.test.js | 1 - .../layers/styles/vector/symbol_utils.test.js | 3 - .../layers/styles/vector/vector_style.js | 1 - x-pack/legacy/plugins/maps/public/meta.js | 1 - yarn.lock | 8 +- 79 files changed, 2067 insertions(+), 1305 deletions(-) create mode 100644 src/legacy/core_plugins/input_control_vis/index.ts rename src/legacy/core_plugins/input_control_vis/public/__snapshots__/{input_control_fn.test.js.snap => input_control_fn.test.ts.snap} (100%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/{controls_tab.test.js.snap => controls_tab.test.tsx.snap} (73%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/{list_control_editor.test.js.snap => list_control_editor.test.tsx.snap} (98%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/{options_tab.test.js.snap => options_tab.test.tsx.snap} (98%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/{range_control_editor.test.js.snap => range_control_editor.test.tsx.snap} (97%) create mode 100644 src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_deps_mock.tsx rename src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/{get_index_pattern_mock.js => get_index_pattern_mock.ts} (82%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/{get_index_patterns_mock.js => get_index_patterns_mock.ts} (100%) create mode 100644 src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_search_service_mock.ts create mode 100644 src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/update_component.ts rename src/legacy/core_plugins/input_control_vis/public/components/editor/{control_editor.js => control_editor.tsx} (72%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/{controls_tab.test.js => controls_tab.test.tsx} (87%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/{controls_tab.js => controls_tab.tsx} (68%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/{field_select.js => field_select.tsx} (70%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/{index_pattern_select_form_row.js => index_pattern_select_form_row.tsx} (73%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/{list_control_editor.test.js => list_control_editor.test.tsx} (80%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/{list_control_editor.js => list_control_editor.tsx} (70%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/{options_tab.test.js => options_tab.test.tsx} (93%) rename src/legacy/core_plugins/input_control_vis/public/components/editor/{options_tab.js => options_tab.tsx} (74%) delete mode 100644 src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.js rename src/legacy/core_plugins/input_control_vis/public/components/editor/{range_control_editor.test.js => range_control_editor.test.tsx} (71%) create mode 100644 src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx rename src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/{form_row.test.js.snap => form_row.test.tsx.snap} (98%) rename src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/{input_control_vis.test.js.snap => input_control_vis.test.tsx.snap} (99%) rename src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/{list_control.test.js.snap => list_control.test.tsx.snap} (98%) rename src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/{range_control.test.js.snap => range_control.test.tsx.snap} (98%) rename src/legacy/core_plugins/input_control_vis/public/components/vis/{form_row.test.js => form_row.test.tsx} (100%) rename src/legacy/core_plugins/input_control_vis/public/components/vis/{form_row.js => form_row.tsx} (75%) rename src/legacy/core_plugins/input_control_vis/public/components/vis/{input_control_vis.test.js => input_control_vis.test.tsx} (87%) rename src/legacy/core_plugins/input_control_vis/public/components/vis/{input_control_vis.js => input_control_vis.tsx} (60%) rename src/legacy/core_plugins/input_control_vis/public/components/vis/{list_control.test.js => list_control.test.tsx} (92%) rename src/legacy/core_plugins/input_control_vis/public/components/vis/{list_control.js => list_control.tsx} (76%) rename src/legacy/core_plugins/input_control_vis/public/components/vis/{range_control.test.js => range_control.test.tsx} (89%) rename src/legacy/core_plugins/input_control_vis/public/components/vis/{range_control.js => range_control.tsx} (72%) rename src/legacy/core_plugins/input_control_vis/public/control/{control.test.js => control.test.ts} (78%) rename src/legacy/core_plugins/input_control_vis/public/control/{control.js => control.ts} (66%) rename src/legacy/core_plugins/input_control_vis/public/control/{control_factory.js => control_factory.ts} (86%) rename src/legacy/core_plugins/input_control_vis/public/control/{create_search_source.js => create_search_source.ts} (71%) rename src/legacy/core_plugins/input_control_vis/public/control/filter_manager/{filter_manager.test.js => filter_manager.test.ts} (66%) rename src/legacy/core_plugins/input_control_vis/public/control/filter_manager/{filter_manager.js => filter_manager.ts} (62%) rename src/legacy/core_plugins/input_control_vis/public/control/filter_manager/{phrase_filter_manager.test.js => phrase_filter_manager.test.ts} (79%) rename src/legacy/core_plugins/input_control_vis/public/control/filter_manager/{phrase_filter_manager.js => phrase_filter_manager.ts} (68%) rename src/legacy/core_plugins/input_control_vis/public/control/filter_manager/{range_filter_manager.test.js => range_filter_manager.test.ts} (68%) rename src/legacy/core_plugins/input_control_vis/public/control/filter_manager/{range_filter_manager.js => range_filter_manager.ts} (77%) rename src/legacy/core_plugins/input_control_vis/public/control/{list_control_factory.test.js => list_control_factory.test.ts} (57%) rename src/legacy/core_plugins/input_control_vis/public/control/{list_control_factory.js => list_control_factory.ts} (63%) rename src/legacy/core_plugins/input_control_vis/public/control/{range_control_factory.test.js => range_control_factory.test.ts} (59%) rename src/legacy/core_plugins/input_control_vis/public/control/{range_control_factory.js => range_control_factory.ts} (63%) rename src/legacy/core_plugins/input_control_vis/public/{editor_utils.js => editor_utils.ts} (64%) rename src/legacy/core_plugins/input_control_vis/{index.js => public/index.ts} (71%) rename src/legacy/core_plugins/input_control_vis/public/{input_control_fn.test.js => input_control_fn.test.ts} (83%) rename src/legacy/core_plugins/input_control_vis/public/{input_control_fn.js => input_control_fn.ts} (70%) create mode 100644 src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts create mode 100644 src/legacy/core_plugins/input_control_vis/public/legacy.ts create mode 100644 src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts rename src/legacy/core_plugins/input_control_vis/public/lineage/{index.js => index.ts} (100%) rename src/legacy/core_plugins/input_control_vis/public/lineage/{lineage_map.test.js => lineage_map.test.ts} (94%) rename src/legacy/core_plugins/input_control_vis/public/lineage/{lineage_map.js => lineage_map.ts} (80%) rename src/legacy/core_plugins/input_control_vis/public/lineage/{parent_candidates.test.js => parent_candidates.test.ts} (98%) rename src/legacy/core_plugins/input_control_vis/public/lineage/{parent_candidates.js => parent_candidates.ts} (85%) create mode 100644 src/legacy/core_plugins/input_control_vis/public/plugin.ts delete mode 100644 src/legacy/core_plugins/input_control_vis/public/register_vis.js delete mode 100644 src/legacy/core_plugins/input_control_vis/public/vis_controller.js create mode 100644 src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index 495b5fb374b4..36bbc8cc8287 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -18,6 +18,7 @@ */ import { PromiseType } from 'utility-types'; +export { $Values, Required, Optional, Class } from 'utility-types'; /** * Returns wrapped type of a promise. diff --git a/packages/kbn-utility-types/package.json b/packages/kbn-utility-types/package.json index a79d08677020..a999eb41eb78 100644 --- a/packages/kbn-utility-types/package.json +++ b/packages/kbn-utility-types/package.json @@ -13,7 +13,7 @@ "clean": "del target" }, "dependencies": { - "utility-types": "^3.7.0" + "utility-types": "^3.10.0" }, "devDependencies": { "del-cli": "^3.0.0", diff --git a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/mode/script_highlight_rules.js b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/mode/script_highlight_rules.js index 801580f4e158..b3999c76493c 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/mode/script_highlight_rules.js +++ b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/mode/script_highlight_rules.js @@ -61,7 +61,6 @@ export function ScriptHighlightRules() { }, { token: 'script.keyword.operator', - regex: '\\?\\.|\\*\\.|=~|==~|!|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|->|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|typeof|void)', }, diff --git a/src/legacy/core_plugins/input_control_vis/index.ts b/src/legacy/core_plugins/input_control_vis/index.ts new file mode 100644 index 000000000000..8f6178e26126 --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/index.ts @@ -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; diff --git a/src/legacy/core_plugins/input_control_vis/public/__snapshots__/input_control_fn.test.js.snap b/src/legacy/core_plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/__snapshots__/input_control_fn.test.js.snap rename to src/legacy/core_plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.js.snap b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap similarity index 73% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.js.snap rename to src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap index 809214f75671..632fe63e9e14 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.js.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap @@ -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]} diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.js.snap b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.tsx.snap similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.js.snap rename to src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.tsx.snap index ff3d1ffc146e..9bc8b1b9ac5c 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.js.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.tsx.snap @@ -3,6 +3,7 @@ exports[`renders dynamic options should display disabled dynamic options with tooltip for non-string fields 1`] = ` { + return fields.find(({ name: n }) => n === name); +}; + +export const getDepsMock = (): InputControlVisDependencies => + ({ + core: { + getStartServices: jest.fn().mockReturnValue([ + null, + { + data: { + ui: { + IndexPatternSelect: () => (
) 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); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_pattern_mock.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_pattern_mock.ts similarity index 82% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_pattern_mock.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_pattern_mock.ts index c693bf100e26..638dd7170cb8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_pattern_mock.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_pattern_mock.ts @@ -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 => { 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); }; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_patterns_mock.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_patterns_mock.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_patterns_mock.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_patterns_mock.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_search_service_mock.ts b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_search_service_mock.ts new file mode 100644 index 000000000000..9da47bedcc78 --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_search_service_mock.ts @@ -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, + }, + ], + }, + }, + } + ), + })); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/update_component.ts b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/update_component.ts new file mode 100644 index 000000000000..881412a7c56f --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/update_component.ts @@ -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, React.Component<{}, {}, any>> + | ReactWrapper, React.Component<{}, {}, any>> +) => { + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); +}; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx similarity index 72% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx index cc31b8d238db..dbac5d9d9437 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx @@ -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) => 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; + handleCheckboxOptionChange: ( + controlIndex: number, + optionName: keyof ControlParamsOptions, + event: EuiSwitchEvent + ) => void; + handleNumberOptionChange: ( + controlIndex: number, + optionName: keyof ControlParamsOptions, + event: ChangeEvent + ) => void; + parentCandidates: Array<{ + value: string; + text: string; + }>; + handleParentChange: (controlIndex: number, event: ChangeEvent) => void; + deps: InputControlVisDependencies; +} + +class ControlEditorUi extends PureComponent { + changeLabel = (event: ChangeEvent) => { + 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 = ( ); break; - case 'range': + case CONTROL_TYPES.RANGE: controlEditor = ( ); 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); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx similarity index 87% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx index 28f435c27ea8..4e7c9bafbf51 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx @@ -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
; - }, - }, - }, - }, - }, -})); - 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, diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx similarity index 68% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx index 97036d7b0f5d..56381ef7d157 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx @@ -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, 'vis' | 'stateParams' | 'setValue'> & { + deps: InputControlVisDependencies; + }; + +export type ControlsTabUiProps = ControlsTabUiInjectedProps; + +class ControlsTabUi extends PureComponent { state = { - type: 'list', + type: CONTROL_TYPES.LIST, }; - getIndexPattern = async indexPatternId => { - return await npStart.plugins.data.indexPatterns.get(indexPatternId); + getIndexPattern = async (indexPatternId: string): Promise => { + 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) => { 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 + ) => { 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) => { 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 +) => ; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx similarity index 70% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx index 456ff17a316a..bde2f09ab0a4 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx @@ -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 { IIndexPattern, IFieldType } from '../../../../../../plugins/data/public'; + +interface FieldSelectUiState { + isLoading: boolean; + fields: Array>; + indexPatternId: string; +} + +export type FieldSelectUiProps = InjectedIntlProps & { + getIndexPattern: (indexPatternId: string) => Promise; + indexPatternId: string; + onChange: (value: any) => void; + fieldName?: string; + filterField?: (field: IFieldType) => boolean; + controlIndex: number; +}; -import { EuiFormRow, EuiComboBox } from '@elastic/eui'; +class FieldSelectUi extends Component { + private hasUnmounted: boolean; -class FieldSelectUi extends Component { - constructor(props) { + 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(); + const fields: Array> = []; + 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>) => { 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); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx similarity index 73% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx index 7d7fbc0539de..66fdbca64f05 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx @@ -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; +}; -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', })} > - ); } -IndexPatternSelectFormRowUi.propTypes = { - onChange: PropTypes.func.isRequired, - indexPatternId: PropTypes.string, - controlIndex: PropTypes.number.isRequired, -}; - export const IndexPatternSelectFormRow = injectI18n(IndexPatternSelectFormRowUi); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx similarity index 80% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx index 24b14943c8bb..de0187f87212 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx @@ -17,31 +17,21 @@ * under the License. */ -jest.mock('ui/new_platform', () => ({ - npStart: { - plugins: { - data: { - ui: { - IndexPatternSelect: () => { - return
; - }, - }, - }, - }, - }, -})); - 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 { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; +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( { /> ); - // 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( { /> ); - // 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( { /> ); - // 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( { /> ); - // 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( { /> ); - // 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( { /> ); - // 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( { /> ); - // 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( { /> ); - 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); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx similarity index 70% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx index 2ee225475b0f..ff74d30a6e1a 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx @@ -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'; +interface ListControlEditorState { + isLoadingFieldType: boolean; + isStringField: boolean; + prevFieldName: string; + IndexPatternSelect: ComponentType | null; +} -function filterField(field) { - return field.aggregatable && ['number', 'boolean', 'date', 'ip', 'string'].includes(field.type); +interface ListControlEditorProps { + getIndexPattern: (indexPatternId: string) => Promise; + 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 + ) => void; + handleParentChange: (controlIndex: number, event: ChangeEvent) => void; + parentCandidates: EuiSelectProps['options']; + deps: InputControlVisDependencies; } -export class ListControlEditor extends Component { - state = { +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 { { - this.props.handleParentChange(this.props.controlIndex, evt); + onChange={event => { + this.props.handleParentChange(this.props.controlIndex, event); }} /> @@ -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 { { - 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 ( - + <> {this.renderOptions()} - + ); } } - -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, -}; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.tsx similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.tsx index ef84d37ca8de..36ec4d4446fd 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.tsx @@ -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(), }; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx similarity index 74% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx index 236624b11118..43f9e15302e5 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx @@ -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'; + +import { VisOptionsProps } from '../../legacy_imports'; + +interface OptionsTabParams { + updateFiltersOnChange: boolean; + useTimeFilter: boolean; + pinFilters: boolean; +} +type OptionsTabInjectedProps = Pick< + VisOptionsProps, + 'vis' | 'setValue' | 'stateParams' +>; + +export type OptionsTabProps = OptionsTabInjectedProps; -export class OptionsTab extends Component { - handleUpdateFiltersChange = evt => { - this.props.setValue('updateFiltersOnChange', evt.target.checked); +export class OptionsTab extends PureComponent { + 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, -}; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.js deleted file mode 100644 index 6e1754b28647..000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.js +++ /dev/null @@ -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 ( - - - - - - - } - > - - - - - } - > - - - - ); -} - -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, -}; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx similarity index 71% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx index 145b18a42dc1..e7f9b6083890 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx @@ -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
; - }, - }, - }, - }, - }, -})); - +// @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( { handleNumberOptionChange={handleNumberOptionChange} /> ); + + await updateComponent(component); + expect(component).toMatchSnapshot(); // eslint-disable-line }); -test('handleNumberOptionChange - step', () => { +test('handleNumberOptionChange - step', async () => { const component = mountWithIntl( { 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( { 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; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx new file mode 100644 index 000000000000..44477eafda6b --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx @@ -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; + handleFieldNameChange: (fieldName: string) => void; + handleIndexPatternChange: (indexPatternId: string) => void; + handleNumberOptionChange: ( + controlIndex: number, + optionName: keyof ControlParamsOptions, + event: ChangeEvent + ) => void; + deps: InputControlVisDependencies; +} + +interface RangeControlEditorState { + IndexPatternSelect: ComponentType | 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 ( + + + + + + + } + > + { + this.props.handleNumberOptionChange(this.props.controlIndex, 'step', event); + }} + data-test-subj={`rangeControlSizeInput${this.props.controlIndex}`} + /> + + + + } + > + { + this.props.handleNumberOptionChange(this.props.controlIndex, 'decimalPlaces', event); + }} + data-test-subj={`rangeControlDecimalPlacesInput${this.props.controlIndex}`} + /> + + + ); + } +} diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.js.snap b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.js.snap rename to src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap index 6437cb19aef9..ba183cc40b12 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.js.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap @@ -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" >
diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.js.snap b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap similarity index 99% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.js.snap rename to src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap index 841421474e7b..5a76967c71fb 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.js.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap @@ -18,7 +18,6 @@ exports[`Apply and Cancel change btns enabled when there are changes 1`] = ` > diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx similarity index 75% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx index fdd4fcb6e26a..29385582924e 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx @@ -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 = ( - + {control} ); @@ -49,12 +57,3 @@ export function FormRow(props) { ); } - -FormRow.propTypes = { - label: PropTypes.string.isRequired, - warningMsg: PropTypes.string, - id: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, - controlIndex: PropTypes.number.isRequired, - disableMsg: PropTypes.string, -}; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx similarity index 87% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx index a835078ab4dc..1712f024f5b7 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx @@ -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'); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx similarity index 60% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx index 9e140155698f..e2497287f35d 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx @@ -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'; + +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; + updateFiltersOnChange?: boolean; + hasChanges: () => boolean; + hasValues: () => boolean; + refreshControl: (controlIndex: number, query: any) => Promise; +} -export class InputControlVis extends Component { - constructor(props) { +export class InputControlVis extends Component { + 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 = ( - { - this.props.refreshControl(index, query); - }} - /> - ); - break; - case 'range': - controlComponent = ( - - ); - break; - default: - throw new Error(`Unhandled control type ${control.type}`); + + if (isListControl(control)) { + controlComponent = ( + { + this.props.refreshControl(index, query); + }} + /> + ); + } else if (isRangeControl(control)) { + controlComponent = ( + + ); + } else { + throw new Error(`Unhandled control type ${control!.type}`); } + return ( { +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', () => { ); expect(component).toMatchSnapshot(); // eslint-disable-line diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx similarity index 76% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx index 7e92545e817e..d62adfdce56b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx @@ -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 { + 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); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx similarity index 89% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx index 8b72def2f269..639616151a39 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx @@ -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( {}} /> ); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx similarity index 72% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx index ee3e3c8fe478..cd3982afd9af 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx @@ -18,12 +18,17 @@ */ import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; + +import { ValidatedDualRange } from '../../legacy_imports'; import { FormRow } from './form_row'; -import { ValidatedDualRange } from 'ui/validated_range'; +import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; -function roundWithPrecision(value, decimalPlaces, roundFunction) { +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]; +} + +export interface RangeControlProps { + control: RangeControlClass; + controlIndex: number; + stageFilter: (controlIndex: number, value: any) => void; +} + +export class RangeControl extends PureComponent { + state: RangeControlState = {}; - static getDerivedStateFromProps(nextProps, prevState) { + 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()} ); } } - -RangeControl.propTypes = { - control: PropTypes.object.isRequired, - controlIndex: PropTypes.number.isRequired, - stageFilter: PropTypes.func.isRequired, -}; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.test.js b/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts similarity index 78% rename from src/legacy/core_plugins/input_control_vis/public/control/control.test.js rename to src/legacy/core_plugins/input_control_vis/public/control/control.test.ts index aa9bed44d031..e76b199a0262 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts @@ -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 { + 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'); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.js b/src/legacy/core_plugins/input_control_vis/public/control/control.ts similarity index 66% rename from src/legacy/core_plugins/input_control_vis/public/control/control.js rename to src/legacy/core_plugins/input_control_vis/public/control/control.ts index 4035dc1adefe..9dc03ecc2345 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/control.ts @@ -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 { + private kbnFilter: esFilters.Filter | null = null; + + enable: boolean = false; + disabledReason: string = ''; + value: any; + + id: string; + options: ControlParamsOptions; + type: CONTROL_TYPES; + label: string; + ancestors: Array> = []; + + 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; - 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>) { 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; } } diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control_factory.js b/src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts similarity index 86% rename from src/legacy/core_plugins/input_control_vis/public/control/control_factory.js rename to src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts index 6d3c7756f72a..3dcc1d53d421 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control_factory.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts @@ -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: diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts similarity index 71% rename from src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js rename to src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts index 2917dda5e96a..c8fa5af5e052 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts @@ -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; }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.js b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts similarity index 66% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.js rename to src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts index 95277ac073d7..fd2cbae121b7 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts @@ -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); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.js b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts similarity index 62% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.js rename to src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts index 672f56746cf8..d80a74ed46ea 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts @@ -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'; + +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() { + 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.'); - } } diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.js b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts similarity index 79% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.js rename to src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts index 7aa1ec663204..dc577ca7168d 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts @@ -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); }); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.js b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts similarity index 68% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.js rename to src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts index 1e60f8c4ebb6..b0b46be86f1e 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts @@ -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; + 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( - this.indexPattern.fields.getByName(this.fieldName), - phrases[0], - this.indexPattern - ); + newFilter = esFilters.buildPhraseFilter(value, phrases[0], this.indexPattern); } else { - newFilter = esFilters.buildPhrasesFilter( - this.indexPattern.fields.getByName(this.fieldName), - phrases, - this.indexPattern - ); + 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.} 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(kbnFilter, 'query.bool.should') + .map(kbnQueryFilter => { + return this.getValueFromFilter(kbnQueryFilter); }) .filter(value => { if (value) { diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.js b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts similarity index 68% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.js rename to src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts index ffe2ebdad53b..f4993a60c5b3 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts @@ -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); }); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.js b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts similarity index 77% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.js rename to src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts index 5a2e7b7d779b..0a6819bd68e6 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts @@ -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 { diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.js b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts similarity index 57% rename from src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.js rename to src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts index 3b5ef7372bc1..242090772763 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts @@ -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.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]; - }, - }, - }), - }, - }, - }, - }, +jest.doMock('./create_search_source.ts', () => ({ + createSearchSource: MockSearchSource, })); -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]); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.js b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts similarity index 63% rename from src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.js rename to src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts index d90b21eead5c..56b42f295ce1 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts @@ -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 { + 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); - - // 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. +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(({ 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; } diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.js b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts similarity index 59% rename from src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.js rename to src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts index b545c6e2834f..5328aeb6c6a4 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts @@ -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); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.js b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts similarity index 63% rename from src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.js rename to src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts index c99c794c1fcd..b9191436b596 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts @@ -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 { + 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 { + 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 ); } diff --git a/src/legacy/core_plugins/input_control_vis/public/editor_utils.js b/src/legacy/core_plugins/input_control_vis/public/editor_utils.ts similarity index 64% rename from src/legacy/core_plugins/input_control_vis/public/editor_utils.js rename to src/legacy/core_plugins/input_control_vis/public/editor_utils.ts index f5b4390342a0..74def0a8d86f 100644 --- a/src/legacy/core_plugins/input_control_vis/public/editor_utils.js +++ b/src/legacy/core_plugins/input_control_vis/public/editor_utils.ts @@ -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; -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}`; diff --git a/src/legacy/core_plugins/input_control_vis/index.js b/src/legacy/core_plugins/input_control_vis/public/index.ts similarity index 71% rename from src/legacy/core_plugins/input_control_vis/index.js rename to src/legacy/core_plugins/input_control_vis/public/index.ts index a21b79e28fb7..e14c2cc4b69b 100644 --- a/src/legacy/core_plugins/input_control_vis/index.js +++ b/src/legacy/core_plugins/input_control_vis/public/index.ts @@ -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); } diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.js b/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts similarity index 83% rename from src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.js rename to src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts index 09c6749bcab9..aa1383587ea6 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts @@ -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(); }); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.js b/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts similarity index 70% rename from src/legacy/core_plugins/input_control_vis/public/input_control_fn.js rename to src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts index 0bd435f502a5..0482c0d2cbff 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.js +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts @@ -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; + +interface RenderValue { + visType: 'input_control_vis'; + visConfig: VisParams; +} + +type Return = Promise>; + +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); diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts new file mode 100644 index 000000000000..b6774aa87b43 --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts @@ -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', + }; +} diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy.ts b/src/legacy/core_plugins/input_control_vis/public/legacy.ts new file mode 100644 index 000000000000..438cdffdb323 --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/legacy.ts @@ -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 = { + expressions: npSetup.plugins.expressions, + data: npSetup.plugins.data, + visualizations: visualizationsSetup, +}; + +const startPlugins: Readonly = { + 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); diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts new file mode 100644 index 000000000000..864ce3b14668 --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts @@ -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; +export const SearchSource = SearchSourceClass; diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/index.js b/src/legacy/core_plugins/input_control_vis/public/lineage/index.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/index.js rename to src/legacy/core_plugins/input_control_vis/public/lineage/index.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.js b/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts similarity index 94% rename from src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.js rename to src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts index de1b589b7dfa..a0cd648007ec 100644 --- a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts @@ -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; diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.js b/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts similarity index 80% rename from src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.js rename to src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts index a08c5d1670a0..d74782c37394 100644 --- a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.js +++ b/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts @@ -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(); 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); + } } }; diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.js b/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.js rename to src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts index fe180357067a..af6e2444b486 100644 --- a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts @@ -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'; diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.js b/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts similarity index 85% rename from src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.js rename to src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts index 17005c24dd41..af4fddef1900 100644 --- a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.js +++ b/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts @@ -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 +) { 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; diff --git a/src/legacy/core_plugins/input_control_vis/public/plugin.ts b/src/legacy/core_plugins/input_control_vis/public/plugin.ts new file mode 100644 index 000000000000..e9ffad8b35f2 --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/plugin.ts @@ -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; + +export interface InputControlVisDependencies { + core: InputControlVisCoreSetup; + data: DataPublicPluginSetup; +} + +/** @internal */ +export interface InputControlVisPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; +} + +/** @internal */ +export interface InputControlVisPluginStartDependencies { + expressions: ReturnType; + visualizations: VisualizationsStart; + data: DataPublicPluginStart; +} + +/** @internal */ +export class InputControlVisPlugin implements Plugin, void> { + constructor(public initializerContext: PluginInitializerContext) {} + + public async setup( + core: InputControlVisCoreSetup, + { expressions, visualizations, data }: InputControlVisPluginSetupDependencies + ) { + const visualizationDependencies: Readonly = { + core, + data, + }; + + expressions.registerFunction(createInputControlVisFn); + visualizations.types.createBaseVisualization( + createInputControlVisTypeDefinition(visualizationDependencies) + ); + } + + public start(core: CoreStart, deps: InputControlVisPluginStartDependencies) { + // nothing to do here + } +} diff --git a/src/legacy/core_plugins/input_control_vis/public/register_vis.js b/src/legacy/core_plugins/input_control_vis/public/register_vis.js deleted file mode 100644 index 09993be3614f..000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/register_vis.js +++ /dev/null @@ -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); diff --git a/src/legacy/core_plugins/input_control_vis/public/vis_controller.js b/src/legacy/core_plugins/input_control_vis/public/vis_controller.js deleted file mode 100644 index 6a1e23769e28..000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/vis_controller.js +++ /dev/null @@ -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( - - - , - 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 }; diff --git a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx b/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx new file mode 100644 index 000000000000..849b58b8ee2d --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx @@ -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; + 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.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(controlFactoryPromises); + + const getControl = (controlId: string) => { + return controls.find(({ id }) => id === controlId); + }; + + const controlInitPromises: Array> = []; + getLineageMap(controlParamsList).forEach((lineage, controlId) => { + // first lineage item is the control. remove it + lineage.shift(); + const ancestors: Array = []; + 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(); + }; + }; +}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index 98581223afa4..2946597581fd 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -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'; diff --git a/src/legacy/ui/public/courier/search_source/types.ts b/src/legacy/ui/public/courier/search_source/types.ts index 293f3d49596c..8fd6d8cfa5fa 100644 --- a/src/legacy/ui/public/courier/search_source/types.ts +++ b/src/legacy/ui/public/courier/search_source/types.ts @@ -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 { diff --git a/src/plugins/data/common/es_query/filters/phrases_filter.ts b/src/plugins/data/common/es_query/filters/phrases_filter.ts index 006e0623be91..2c964097e7c0 100644 --- a/src/plugins/data/common/es_query/filters/phrases_filter.ts +++ b/src/plugins/data/common/es_query/filters/phrases_filter.ts @@ -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; diff --git a/src/plugins/data/public/index_patterns/lib/get_from_saved_object.ts b/src/plugins/data/public/index_patterns/lib/get_from_saved_object.ts index 0faf6f4a1034..60b2023f2560 100644 --- a/src/plugins/data/public/index_patterns/lib/get_from_saved_object.ts +++ b/src/plugins/data/public/index_patterns/lib/get_from_saved_object.ts @@ -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; } diff --git a/src/plugins/data/public/query/timefilter/get_time.ts b/src/plugins/data/public/query/timefilter/get_time.ts index d3fbc17734f8..76f39da1cf70 100644 --- a/src/plugins/data/public/query/timefilter/get_time.ts +++ b/src/plugins/data/public/query/timefilter/get_time.ts @@ -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 ); diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index ad453c4e5d11..829c8205a8b5 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -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, '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 { 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 { return ( { options={this.state.options} selectedOptions={this.state.selectedIndexPattern ? [this.state.selectedIndexPattern] : []} onChange={this.onChange} - {...rest} /> ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js index 1ea065c7e9e6..8826c771fab1 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.test.js @@ -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%)' ); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js index fd4794b385bd..ed59b1d5513a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js @@ -27,7 +27,6 @@ describe('styleSvg', () => { const unstyledSvgString = ''; const styledSvg = await styleSvg(unstyledSvgString, 'red'); - expect(styledSvg.split('\n')[1]).toBe( '' ); @@ -37,7 +36,6 @@ describe('styleSvg', () => { const unstyledSvgString = ''; const styledSvg = await styleSvg(unstyledSvgString, 'red', 'white'); - expect(styledSvg.split('\n')[1]).toBe( '' ); @@ -47,7 +45,6 @@ describe('styleSvg', () => { const unstyledSvgString = ''; const styledSvg = await styleSvg(unstyledSvgString, 'red', 'white', '2px'); - expect(styledSvg.split('\n')[1]).toBe( '' ); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 1a547e04da8a..65698c67a229 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -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 diff --git a/x-pack/legacy/plugins/maps/public/meta.js b/x-pack/legacy/plugins/maps/public/meta.js index d92b8713f0e7..7cdb8d67c057 100644 --- a/x-pack/legacy/plugins/maps/public/meta.js +++ b/x-pack/legacy/plugins/maps/public/meta.js @@ -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'); diff --git a/yarn.lock b/yarn.lock index dea42c933661..dff0e9d46e7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"