diff --git a/package.json b/package.json index aa75fa483f515..778b0d60a886e 100644 --- a/package.json +++ b/package.json @@ -169,6 +169,7 @@ "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.1", "inert": "^5.1.0", + "inline-style": "^2.0.0", "joi": "^13.5.2", "jquery": "^3.4.1", "js-yaml": "3.13.1", diff --git a/packages/kbn-es-query/src/filters/lib/meta_filter.ts b/packages/kbn-es-query/src/filters/lib/meta_filter.ts index 48ec04503fde0..c11fb592f0e29 100644 --- a/packages/kbn-es-query/src/filters/lib/meta_filter.ts +++ b/packages/kbn-es-query/src/filters/lib/meta_filter.ts @@ -38,7 +38,7 @@ export interface FilterMeta { } export interface Filter { - $state: FilterState; + $state?: FilterState; meta: FilterMeta; query?: object; } @@ -62,7 +62,7 @@ export function buildEmptyFilter(isPinned: boolean, index?: string): Filter { } export function isFilterPinned(filter: Filter) { - return filter.$state.store === FilterStateStore.GLOBAL_STATE; + return filter.$state && filter.$state.store === FilterStateStore.GLOBAL_STATE; } export function toggleFilterDisabled(filter: Filter) { diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/directive.js b/src/legacy/core_plugins/data/public/filter/apply_filters/directive.js index a16d17519eb73..f235bb3dab600 100644 --- a/src/legacy/core_plugins/data/public/filter/apply_filters/directive.js +++ b/src/legacy/core_plugins/data/public/filter/apply_filters/directive.js @@ -21,7 +21,7 @@ import 'ngreact'; import { uiModules } from 'ui/modules'; import template from './directive.html'; import { ApplyFiltersPopover } from './apply_filters_popover'; -import { mapAndFlattenFilters } from 'ui/filter_manager/lib/map_and_flatten_filters'; +import { mapAndFlattenFilters } from '../filter_manager/lib/map_and_flatten_filters'; import { wrapInI18nContext } from 'ui/i18n'; const app = uiModules.get('app/data', ['react']); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx index 6ca27c31bd357..85b0c7cd06049 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx @@ -441,17 +441,20 @@ class FilterEditorUI extends Component { queryDsl, } = this.state; - const { store } = this.props.filter.$state; + const { $state } = this.props.filter; + if (!$state || !$state.store) { + return; // typescript validation + } const alias = useCustomLabel ? customLabel : null; if (isCustomEditorOpen) { const { index, disabled, negate } = this.props.filter.meta; const newIndex = index || this.props.indexPatterns[0].id; const body = JSON.parse(queryDsl); - const filter = buildCustomFilter(newIndex, body, disabled, negate, alias, store); + const filter = buildCustomFilter(newIndex, body, disabled, negate, alias, $state.store); this.props.onSubmit(filter); } else if (indexPattern && field && operator) { - const filter = buildFilter(indexPattern, field, operator, params, alias, store); + const filter = buildFilter(indexPattern, field, operator, params, alias, $state.store); this.props.onSubmit(filter); } }; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts index 859f2fcc1535a..d3006ceb10a2c 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts @@ -239,7 +239,11 @@ describe('Filter editor utils', () => { const filter = buildFilter(mockIndexPattern, mockFields[0], isOperator, params, alias, state); expect(filter.meta.negate).toBe(isOperator.negate); expect(filter.meta.alias).toBe(alias); - expect(filter.$state.store).toBe(state); + + expect(filter.$state).toBeDefined(); + if (filter.$state) { + expect(filter.$state.store).toBe(state); + } }); it('should build phrases filters', () => { @@ -257,7 +261,10 @@ describe('Filter editor utils', () => { expect(filter.meta.type).toBe(isOneOfOperator.type); expect(filter.meta.negate).toBe(isOneOfOperator.negate); expect(filter.meta.alias).toBe(alias); - expect(filter.$state.store).toBe(state); + expect(filter.$state).toBeDefined(); + if (filter.$state) { + expect(filter.$state.store).toBe(state); + } }); it('should build range filters', () => { @@ -274,7 +281,10 @@ describe('Filter editor utils', () => { ); expect(filter.meta.negate).toBe(isBetweenOperator.negate); expect(filter.meta.alias).toBe(alias); - expect(filter.$state.store).toBe(state); + expect(filter.$state).toBeDefined(); + if (filter.$state) { + expect(filter.$state.store).toBe(state); + } }); it('should build exists filters', () => { @@ -291,7 +301,10 @@ describe('Filter editor utils', () => { ); expect(filter.meta.negate).toBe(existsOperator.negate); expect(filter.meta.alias).toBe(alias); - expect(filter.$state.store).toBe(state); + expect(filter.$state).toBeDefined(); + if (filter.$state) { + expect(filter.$state.store).toBe(state); + } }); it('should negate based on operator', () => { @@ -308,7 +321,10 @@ describe('Filter editor utils', () => { ); expect(filter.meta.negate).toBe(doesNotExistOperator.negate); expect(filter.meta.alias).toBe(alias); - expect(filter.$state.store).toBe(state); + expect(filter.$state).toBeDefined(); + if (filter.$state) { + expect(filter.$state.store).toBe(state); + } }); }); }); diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.test.ts new file mode 100644 index 0000000000000..5df1e286fa289 --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.test.ts @@ -0,0 +1,683 @@ +/* + * 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 _ from 'lodash'; +import sinon from 'sinon'; + +import { Subscription } from 'rxjs'; +import { Filter, FilterStateStore } from '@kbn/es-query'; + +import { FilterStateManager } from './filter_state_manager'; +import { FilterManager } from './filter_manager'; + +import { getFilter } from './test_helpers/get_stub_filter'; +import { StubIndexPatterns } from './test_helpers/stub_index_pattern'; +import { StubState } from './test_helpers/stub_state'; +import { getFiltersArray } from './test_helpers/get_filters_array'; + +jest.mock( + 'ui/chrome', + () => ({ + getBasePath: jest.fn(() => 'path'), + getUiSettingsClient: jest.fn(() => { + return { + get: () => true, + }; + }), + }), + { virtual: true } +); + +jest.mock('ui/new_platform', () => ({ + npStart: { + core: { + chrome: { + recentlyAccessed: false, + }, + }, + }, + npSetup: { + core: { + uiSettings: { + get: () => true, + }, + }, + }, +})); + +describe('filter_manager', () => { + let appStateStub: StubState; + let globalStateStub: StubState; + + let updateSubscription: Subscription | undefined; + let fetchSubscription: Subscription | undefined; + let updateListener: sinon.SinonSpy; + + let filterManager: FilterManager; + let indexPatterns: any; + let readyFilters: Filter[]; + + beforeEach(() => { + updateListener = sinon.stub(); + appStateStub = new StubState(); + globalStateStub = new StubState(); + indexPatterns = new StubIndexPatterns(); + filterManager = new FilterManager(indexPatterns); + readyFilters = getFiltersArray(); + + // FilterStateManager is tested indirectly. + // Therefore, we don't need it's instance. + new FilterStateManager( + globalStateStub, + () => { + return appStateStub; + }, + filterManager + ); + }); + + afterEach(async () => { + if (updateSubscription) { + updateSubscription.unsubscribe(); + } + if (fetchSubscription) { + fetchSubscription.unsubscribe(); + } + + await filterManager.removeAll(); + }); + + describe('observing', () => { + test('should return observable', () => { + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + fetchSubscription = filterManager.getUpdates$().subscribe(() => {}); + expect(updateSubscription).toBeInstanceOf(Subscription); + expect(fetchSubscription).toBeInstanceOf(Subscription); + }); + + test('should observe global state', done => { + updateSubscription = filterManager.getUpdates$().subscribe(() => { + expect(filterManager.getGlobalFilters()).toHaveLength(1); + if (updateSubscription) { + updateSubscription.unsubscribe(); + } + done(); + }); + + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, true, true, 'age', 34); + globalStateStub.filters.push(f1); + }); + + test('should observe app state', done => { + updateSubscription = filterManager.getUpdates$().subscribe(() => { + expect(filterManager.getAppFilters()).toHaveLength(1); + if (updateSubscription) { + updateSubscription.unsubscribe(); + } + done(); + }); + + const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34); + appStateStub.filters.push(f1); + }); + }); + + describe('get \\ set filters', () => { + test('should be empty', () => { + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + expect(filterManager.getAppFilters()).toHaveLength(0); + expect(filterManager.getGlobalFilters()).toHaveLength(0); + expect(filterManager.getFilters()).toHaveLength(0); + + const partitionedFiltres = filterManager.getPartitionedFilters(); + expect(partitionedFiltres.appFilters).toHaveLength(0); + expect(partitionedFiltres.globalFilters).toHaveLength(0); + expect(updateListener.called).toBeFalsy(); + }); + + test('app state should be set', async () => { + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34); + await filterManager.setFilters([f1]); + expect(filterManager.getAppFilters()).toHaveLength(1); + expect(filterManager.getGlobalFilters()).toHaveLength(0); + expect(filterManager.getFilters()).toHaveLength(1); + + const partitionedFiltres = filterManager.getPartitionedFilters(); + expect(partitionedFiltres.appFilters).toHaveLength(1); + expect(partitionedFiltres.globalFilters).toHaveLength(0); + expect(updateListener.called).toBeTruthy(); + }); + + test('global state should be set', async () => { + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + await filterManager.setFilters([f1]); + expect(filterManager.getAppFilters()).toHaveLength(0); + expect(filterManager.getGlobalFilters()).toHaveLength(1); + expect(filterManager.getFilters()).toHaveLength(1); + + const partitionedFiltres = filterManager.getPartitionedFilters(); + expect(partitionedFiltres.appFilters).toHaveLength(0); + expect(partitionedFiltres.globalFilters).toHaveLength(1); + expect(updateListener.called).toBeTruthy(); + }); + + test('both states should be set', async () => { + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE'); + await filterManager.setFilters([f1, f2]); + expect(filterManager.getAppFilters()).toHaveLength(1); + expect(filterManager.getGlobalFilters()).toHaveLength(1); + expect(filterManager.getFilters()).toHaveLength(2); + + const partitionedFiltres = filterManager.getPartitionedFilters(); + expect(partitionedFiltres.appFilters).toHaveLength(1); + expect(partitionedFiltres.globalFilters).toHaveLength(1); + + // listener should be called just once + expect(updateListener.called).toBeTruthy(); + expect(updateListener.callCount).toBe(1); + }); + + test('set state should override previous state', async () => { + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE'); + + await filterManager.setFilters([f1]); + await filterManager.setFilters([f2]); + + expect(filterManager.getAppFilters()).toHaveLength(1); + expect(filterManager.getGlobalFilters()).toHaveLength(0); + expect(filterManager.getFilters()).toHaveLength(1); + + const partitionedFiltres = filterManager.getPartitionedFilters(); + expect(partitionedFiltres.appFilters).toHaveLength(1); + expect(partitionedFiltres.globalFilters).toHaveLength(0); + + // listener should be called just once + expect(updateListener.called).toBeTruthy(); + expect(updateListener.callCount).toBe(2); + }); + }); + + describe('add filters', async () => { + test('app state should accept a single filter', async function() { + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34); + await filterManager.addFilters(f1); + expect(filterManager.getAppFilters()).toHaveLength(1); + expect(filterManager.getGlobalFilters()).toHaveLength(0); + expect(updateListener.callCount).toBe(1); + expect(appStateStub.filters.length).toBe(1); + }); + + test('app state should accept array', async () => { + const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34); + const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'female'); + await filterManager.addFilters([f1]); + await filterManager.addFilters([f2]); + expect(filterManager.getAppFilters()).toHaveLength(2); + expect(filterManager.getGlobalFilters()).toHaveLength(0); + expect(appStateStub.filters.length).toBe(2); + }); + + test('global state should accept a single filer', async () => { + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + await filterManager.addFilters(f1); + expect(filterManager.getAppFilters()).toHaveLength(0); + expect(filterManager.getGlobalFilters()).toHaveLength(1); + expect(updateListener.callCount).toBe(1); + expect(globalStateStub.filters.length).toBe(1); + }); + + test('global state should be accept array', async () => { + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'gender', 'female'); + await filterManager.addFilters([f1, f2]); + expect(filterManager.getAppFilters()).toHaveLength(0); + expect(filterManager.getGlobalFilters()).toHaveLength(2); + expect(globalStateStub.filters.length).toBe(2); + }); + + test('add multiple filters at once', async () => { + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'gender', 'female'); + await filterManager.addFilters([f1, f2]); + expect(filterManager.getAppFilters()).toHaveLength(0); + expect(filterManager.getGlobalFilters()).toHaveLength(2); + expect(updateListener.callCount).toBe(1); + }); + + test('add same filter to global and app', async () => { + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34); + await filterManager.addFilters([f1, f2]); + + // FILTER SHOULD BE ADDED ONLY ONCE, TO GLOBAL + expect(filterManager.getAppFilters()).toHaveLength(0); + expect(filterManager.getGlobalFilters()).toHaveLength(1); + expect(updateListener.callCount).toBe(1); + }); + + test('add same filter with different values to global and app', async () => { + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 38); + const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34); + await filterManager.addFilters([f1, f2]); + + // FILTER SHOULD BE ADDED TWICE + expect(filterManager.getAppFilters()).toHaveLength(1); + expect(filterManager.getGlobalFilters()).toHaveLength(1); + expect(updateListener.callCount).toBe(1); + }); + + test('add filter with no state, and force pin', async () => { + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 38); + f1.$state = undefined; + + await filterManager.addFilters([f1], true); + + // FILTER SHOULD BE GLOBAL + const f1Output = filterManager.getFilters()[0]; + expect(f1Output.$state).toBeDefined(); + if (f1Output.$state) { + expect(f1Output.$state.store).toBe(FilterStateStore.GLOBAL_STATE); + } + }); + + test('add filter with no state, and dont force pin', async () => { + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 38); + f1.$state = undefined; + + await filterManager.addFilters([f1], false); + + // FILTER SHOULD BE APP + const f1Output = filterManager.getFilters()[0]; + expect(f1Output.$state).toBeDefined(); + if (f1Output.$state) { + expect(f1Output.$state.store).toBe(FilterStateStore.APP_STATE); + } + }); + + test('should return app and global filters', async function() { + const filters = getFiltersArray(); + await filterManager.addFilters(filters[0], false); + await filterManager.addFilters(filters[1], true); + + // global filters should be listed first + let res = filterManager.getFilters(); + expect(res).toHaveLength(2); + expect(res[0].$state && res[0].$state.store).toEqual(FilterStateStore.GLOBAL_STATE); + expect(res[0].meta.disabled).toEqual(filters[1].meta.disabled); + expect(res[0].query).toEqual(filters[1].query); + + expect(res[1].$state && res[1].$state.store).toEqual(FilterStateStore.APP_STATE); + expect(res[1].meta.disabled).toEqual(filters[0].meta.disabled); + expect(res[1].query).toEqual(filters[0].query); + + // should return updated version of filters + await filterManager.addFilters(filters[2], false); + + res = filterManager.getFilters(); + expect(res).toHaveLength(3); + }); + + test('should skip appStateStub filters that match globalStateStub filters', async function() { + await filterManager.addFilters(readyFilters, true); + const appFilter = _.cloneDeep(readyFilters[1]); + await filterManager.addFilters(appFilter, false); + + // global filters should be listed first + const res = filterManager.getFilters(); + expect(res).toHaveLength(3); + _.each(res, function(filter) { + expect(filter.$state && filter.$state.store).toBe(FilterStateStore.GLOBAL_STATE); + }); + }); + + test('should allow overwriting a positive filter by a negated one', async function() { + // Add negate: false version of the filter + const filter = _.cloneDeep(readyFilters[0]); + filter.meta.negate = false; + + await filterManager.addFilters(filter); + expect(filterManager.getFilters()).toHaveLength(1); + expect(filterManager.getFilters()[0]).toEqual(filter); + + // Add negate: true version of the same filter + const negatedFilter = _.cloneDeep(readyFilters[0]); + negatedFilter.meta.negate = true; + + await filterManager.addFilters(negatedFilter); + // The negated filter should overwrite the positive one + expect(globalStateStub.filters.length).toBe(1); + expect(filterManager.getFilters()).toHaveLength(1); + expect(filterManager.getFilters()[0]).toEqual(negatedFilter); + }); + + test('should allow overwriting a negated filter by a positive one', async function() { + // Add negate: true version of the same filter + const negatedFilter = _.cloneDeep(readyFilters[0]); + negatedFilter.meta.negate = true; + + await filterManager.addFilters(negatedFilter); + + // The negated filter should overwrite the positive one + expect(globalStateStub.filters.length).toBe(1); + expect(globalStateStub.filters[0]).toEqual(negatedFilter); + + // Add negate: false version of the filter + const filter = _.cloneDeep(readyFilters[0]); + filter.meta.negate = false; + + await filterManager.addFilters(filter); + expect(globalStateStub.filters.length).toBe(1); + expect(globalStateStub.filters[0]).toEqual(filter); + }); + + test('should fire the update and fetch events', async function() { + const updateStub = sinon.stub(); + const fetchStub = sinon.stub(); + + filterManager.getUpdates$().subscribe({ + next: updateStub, + }); + + filterManager.getFetches$().subscribe({ + next: fetchStub, + }); + + await filterManager.addFilters(readyFilters); + + // updates should trigger state saves + expect(appStateStub.save.callCount).toBe(1); + expect(globalStateStub.save.callCount).toBe(1); + + // this time, events should be emitted + expect(fetchStub.called); + expect(updateStub.called); + }); + }); + + describe('filter reconciliation', function() { + test('should de-dupe appStateStub filters being added', async function() { + const newFilter = _.cloneDeep(readyFilters[1]); + await filterManager.addFilters(readyFilters, false); + expect(appStateStub.filters.length).toBe(3); + + await filterManager.addFilters(newFilter, false); + expect(appStateStub.filters.length).toBe(3); + }); + + test('should de-dupe globalStateStub filters being added', async function() { + const newFilter = _.cloneDeep(readyFilters[1]); + await filterManager.addFilters(readyFilters, true); + expect(globalStateStub.filters.length).toBe(3); + + await filterManager.addFilters(newFilter, true); + expect(globalStateStub.filters.length).toBe(3); + }); + + test('should mutate global filters on appStateStub filter changes', async function() { + const idx = 1; + await filterManager.addFilters(readyFilters, true); + + const appFilter = _.cloneDeep(readyFilters[idx]); + appFilter.meta.negate = true; + appFilter.$state = { + store: FilterStateStore.APP_STATE, + }; + await filterManager.addFilters(appFilter); + const res = filterManager.getFilters(); + expect(res).toHaveLength(3); + _.each(res, function(filter, i) { + expect(filter.$state && filter.$state.store).toBe('globalState'); + // make sure global filter actually mutated + expect(filter.meta.negate).toBe(i === idx); + }); + }); + + test('should merge conflicting appStateStub filters', async function() { + await filterManager.addFilters(readyFilters, true); + const appFilter = _.cloneDeep(readyFilters[1]); + appFilter.meta.negate = true; + appFilter.$state = { + store: FilterStateStore.APP_STATE, + }; + await filterManager.addFilters(appFilter, false); + + // global filters should be listed first + const res = filterManager.getFilters(); + expect(res).toHaveLength(3); + expect( + res.filter(function(filter) { + return filter.$state && filter.$state.store === FilterStateStore.GLOBAL_STATE; + }).length + ).toBe(3); + }); + + test('should enable disabled filters - global state', async function() { + // test adding to globalStateStub + const disabledFilters = _.map(readyFilters, function(filter) { + const f = _.cloneDeep(filter); + f.meta.disabled = true; + return f; + }); + await filterManager.addFilters(disabledFilters, true); + await filterManager.addFilters(readyFilters, true); + + const res = filterManager.getFilters(); + expect(res).toHaveLength(3); + expect( + res.filter(function(filter) { + return filter.meta.disabled === false; + }).length + ).toBe(3); + }); + + test('should enable disabled filters - app state', async function() { + // test adding to appStateStub + const disabledFilters = _.map(readyFilters, function(filter) { + const f = _.cloneDeep(filter); + f.meta.disabled = true; + return f; + }); + await filterManager.addFilters(disabledFilters, true); + await filterManager.addFilters(readyFilters, false); + + const res = filterManager.getFilters(); + expect(res).toHaveLength(3); + expect( + res.filter(function(filter) { + return filter.meta.disabled === false; + }).length + ).toBe(3); + }); + }); + + describe('remove filters', async () => { + test('remove on empty should do nothing and not fire events', async () => { + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + await filterManager.removeAll(); + expect(updateListener.called).toBeFalsy(); + expect(filterManager.getFilters()).toHaveLength(0); + }); + + test('remove on full should clean and fire events', async () => { + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE'); + await filterManager.setFilters([f1, f2]); + + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + await filterManager.removeAll(); + expect(updateListener.called).toBeTruthy(); + expect(filterManager.getFilters()).toHaveLength(0); + }); + + test('remove non existing filter should do nothing and not fire events', async () => { + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE'); + const f3 = getFilter(FilterStateStore.APP_STATE, false, false, 'country', 'US'); + await filterManager.setFilters([f1, f2]); + expect(filterManager.getFilters()).toHaveLength(2); + + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + await filterManager.removeFilter(f3); + expect(updateListener.called).toBeFalsy(); + expect(filterManager.getFilters()).toHaveLength(2); + }); + + test('remove existing filter should remove and fire events', async () => { + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE'); + const f3 = getFilter(FilterStateStore.APP_STATE, false, false, 'country', 'US'); + await filterManager.setFilters([f1, f2, f3]); + expect(filterManager.getFilters()).toHaveLength(3); + + updateSubscription = filterManager.getUpdates$().subscribe(updateListener); + await filterManager.removeFilter(f3); + expect(updateListener.called).toBeTruthy(); + expect(filterManager.getFilters()).toHaveLength(2); + }); + + test('should remove the filter from appStateStub', async function() { + await filterManager.addFilters(readyFilters, false); + expect(appStateStub.filters).toHaveLength(3); + filterManager.removeFilter(readyFilters[0]); + expect(appStateStub.filters).toHaveLength(2); + }); + + test('should remove the filter from globalStateStub', async function() { + await filterManager.addFilters(readyFilters, true); + expect(globalStateStub.filters).toHaveLength(3); + filterManager.removeFilter(readyFilters[0]); + expect(globalStateStub.filters).toHaveLength(2); + }); + + test('should fire the update and fetch events', async function() { + const updateStub = sinon.stub(); + const fetchStub = sinon.stub(); + + await filterManager.addFilters(readyFilters, false); + + filterManager.getUpdates$().subscribe({ + next: updateStub, + }); + + filterManager.getFetches$().subscribe({ + next: fetchStub, + }); + + filterManager.removeFilter(readyFilters[0]); + + // this time, events should be emitted + expect(fetchStub.called); + expect(updateStub.called); + }); + + test('should remove matching filters', async function() { + await filterManager.addFilters([readyFilters[0], readyFilters[1]], true); + await filterManager.addFilters([readyFilters[2]], false); + + filterManager.removeFilter(readyFilters[0]); + + expect(globalStateStub.filters).toHaveLength(1); + expect(appStateStub.filters).toHaveLength(1); + }); + + test('should remove matching filters by comparison', async function() { + await filterManager.addFilters([readyFilters[0], readyFilters[1]], true); + await filterManager.addFilters([readyFilters[2]], false); + + filterManager.removeFilter(_.cloneDeep(readyFilters[0])); + + expect(globalStateStub.filters).toHaveLength(1); + expect(appStateStub.filters).toHaveLength(1); + + filterManager.removeFilter(_.cloneDeep(readyFilters[2])); + expect(globalStateStub.filters).toHaveLength(1); + expect(appStateStub.filters).toHaveLength(0); + }); + + test('should do nothing with a non-matching filter', async function() { + await filterManager.addFilters([readyFilters[0], readyFilters[1]], true); + await filterManager.addFilters([readyFilters[2]], false); + + const missedFilter = _.cloneDeep(readyFilters[0]); + missedFilter.meta.negate = !readyFilters[0].meta.negate; + + filterManager.removeFilter(missedFilter); + expect(globalStateStub.filters).toHaveLength(2); + expect(appStateStub.filters).toHaveLength(1); + }); + + test('should remove all the filters from both states', async function() { + await filterManager.addFilters([readyFilters[0], readyFilters[1]], true); + await filterManager.addFilters([readyFilters[2]], false); + expect(globalStateStub.filters).toHaveLength(2); + expect(appStateStub.filters).toHaveLength(1); + + await filterManager.removeAll(); + expect(globalStateStub.filters).toHaveLength(0); + expect(appStateStub.filters).toHaveLength(0); + }); + }); + + describe('invert', () => { + test('invert to disabled', async () => { + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + filterManager.invertFilter(f1); + expect(f1.meta.negate).toBe(true); + filterManager.invertFilter(f1); + expect(f1.meta.negate).toBe(false); + }); + + test('should fire the update and fetch events', function() { + const updateStub = sinon.stub(); + const fetchStub = sinon.stub(); + + filterManager.addFilters(readyFilters); + filterManager.getUpdates$().subscribe({ + next: updateStub, + }); + + filterManager.getFetches$().subscribe({ + next: fetchStub, + }); + + filterManager.invertFilter(readyFilters[1]); + expect(fetchStub.called); + expect(updateStub.called); + }); + }); + + describe('addFiltersAndChangeTimeFilter', () => { + test('should just add filters if there is no time filter in array', async () => { + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + await filterManager.addFiltersAndChangeTimeFilter([f1]); + expect(filterManager.getFilters()).toHaveLength(1); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.ts new file mode 100644 index 0000000000000..de765c0200365 --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.ts @@ -0,0 +1,212 @@ +/* + * 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 { Filter, isFilterPinned, FilterStateStore } from '@kbn/es-query'; + +import _ from 'lodash'; +import { Subject, Subscription } from 'rxjs'; + +import { npSetup } from 'ui/new_platform'; + +// @ts-ignore +import { compareFilters } from './lib/compare_filters'; +// @ts-ignore +import { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; +// @ts-ignore +import { uniqFilters } from './lib/uniq_filters'; +// @ts-ignore +import { extractTimeFilter } from './lib/extract_time_filter'; +// @ts-ignore +import { changeTimeFilter } from './lib/change_time_filter'; + +import { PartitionedFilters } from './partitioned_filters'; + +import { IndexPatterns } from '../../index_patterns'; + +export class FilterManager { + private indexPatterns: IndexPatterns; + private filters: Filter[] = []; + private updated$: Subject = new Subject(); + private fetch$: Subject = new Subject(); + private updateSubscription$: Subscription | undefined; + + constructor(indexPatterns: IndexPatterns) { + this.indexPatterns = indexPatterns; + } + + destroy() { + if (this.updateSubscription$) { + this.updateSubscription$.unsubscribe(); + } + } + + private mergeIncomingFilters(partitionedFilters: PartitionedFilters): Filter[] { + const globalFilters = partitionedFilters.globalFilters; + const appFilters = partitionedFilters.appFilters; + + // existing globalFilters should be mutated by appFilters + _.each(appFilters, function(filter, i) { + const match = _.find(globalFilters, function(globalFilter) { + return compareFilters(globalFilter, filter); + }); + + // no match, do nothing + if (!match) return; + + // matching filter in globalState, update global and remove from appState + _.assign(match.meta, filter.meta); + appFilters.splice(i, 1); + }); + + return uniqFilters(appFilters.reverse().concat(globalFilters.reverse())).reverse(); + } + + private filtersUpdated(newFilters: Filter[]): boolean { + return !_.isEqual(this.filters, newFilters); + } + + private static partitionFilters(filters: Filter[]): PartitionedFilters { + const [globalFilters, appFilters] = _.partition(filters, isFilterPinned); + return { + globalFilters, + appFilters, + }; + } + + private handleStateUpdate(newFilters: Filter[]) { + // This is where the angular update magic \ syncing diget happens + const filtersUpdated = this.filtersUpdated(newFilters); + + // global filters should always be first + newFilters.sort( + (a: Filter, b: Filter): number => { + if (a.$state && a.$state.store === FilterStateStore.GLOBAL_STATE) { + return -1; + } else if (b.$state && b.$state.store === FilterStateStore.GLOBAL_STATE) { + return 1; + } else { + return 0; + } + } + ); + + this.filters = newFilters; + if (filtersUpdated) { + this.updated$.next(); + // Fired together with updated$, because historically (~4 years ago) there was a fetch optimization, that didn't call fetch for very specific cases. + // This optimization seems irrelevant at the moment, but I didn't want to change the logic of all consumers. + this.fetch$.next(); + } + } + + /* Getters */ + + public getFilters() { + return this.filters; + } + + public getAppFilters() { + const { appFilters } = this.getPartitionedFilters(); + return appFilters; + } + + public getGlobalFilters() { + const { globalFilters } = this.getPartitionedFilters(); + return globalFilters; + } + + public getPartitionedFilters(): PartitionedFilters { + return FilterManager.partitionFilters(this.filters); + } + + public getUpdates$() { + return this.updated$.asObservable(); + } + + public getFetches$() { + return this.fetch$.asObservable(); + } + + /* Setters */ + + public async addFilters(filters: Filter[] | Filter, pinFilterStatus?: boolean) { + if (!Array.isArray(filters)) { + filters = [filters]; + } + + const { uiSettings } = npSetup.core; + if (pinFilterStatus === undefined) { + pinFilterStatus = uiSettings.get('filters:pinnedByDefault'); + } + + // set the store of all filters + // TODO: is this necessary? + const store = pinFilterStatus ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE; + FilterManager.setFiltersStore(filters, store); + + const mappedFilters = await mapAndFlattenFilters(this.indexPatterns, filters); + const newPartitionedFilters = FilterManager.partitionFilters(mappedFilters); + const partitionFilters = this.getPartitionedFilters(); + partitionFilters.appFilters.push(...newPartitionedFilters.appFilters); + partitionFilters.globalFilters.push(...newPartitionedFilters.globalFilters); + + const newFilters = this.mergeIncomingFilters(partitionFilters); + this.handleStateUpdate(newFilters); + } + + public async setFilters(newFilters: Filter[]) { + const mappedFilters = await mapAndFlattenFilters(this.indexPatterns, newFilters); + this.handleStateUpdate(mappedFilters); + } + + public removeFilter(filter: Filter) { + const filterIndex = _.findIndex(this.filters, item => { + return _.isEqual(item.meta, filter.meta) && _.isEqual(item.query, filter.query); + }); + + if (filterIndex >= 0) { + const newFilters = _.cloneDeep(this.filters); + newFilters.splice(filterIndex, 1); + this.handleStateUpdate(newFilters); + } + } + + public invertFilter(filter: Filter) { + filter.meta.negate = !filter.meta.negate; + } + + public async removeAll() { + await this.setFilters([]); + } + + public async addFiltersAndChangeTimeFilter(filters: Filter[]) { + const timeFilter = await extractTimeFilter(this.indexPatterns, filters); + if (timeFilter) changeTimeFilter(timeFilter); + return this.addFilters(filters.filter(filter => filter !== timeFilter)); + } + + public static setFiltersStore(filters: Filter[], store: FilterStateStore) { + _.map(filters, (filter: Filter) => { + // Override status only for filters that didn't have state in the first place. + if (filter.$state === undefined) { + filter.$state = { store }; + } + }); + } +} diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts new file mode 100644 index 0000000000000..c32570500c305 --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts @@ -0,0 +1,126 @@ +/* + * 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 sinon from 'sinon'; + +import { FilterStateStore } from '@kbn/es-query'; + +import { Subscription } from 'rxjs'; +import { FilterStateManager } from './filter_state_manager'; + +import { StubState } from './test_helpers/stub_state'; +import { getFilter } from './test_helpers/get_stub_filter'; +import { FilterManager } from './filter_manager'; +import { StubIndexPatterns } from './test_helpers/stub_index_pattern'; + +jest.mock('ui/new_platform', () => ({ + npStart: { + core: { + chrome: { + recentlyAccessed: false, + }, + }, + }, + npSetup: { + core: { + uiSettings: { + get: () => true, + }, + }, + }, +})); + +describe('filter_state_manager', () => { + let appStateStub: StubState; + let globalStateStub: StubState; + + let subscription: Subscription | undefined; + let filterManager: FilterManager; + + beforeEach(() => { + appStateStub = new StubState(); + globalStateStub = new StubState(); + const indexPatterns = new StubIndexPatterns(); + filterManager = new FilterManager(indexPatterns); + + // FilterStateManager is tested indirectly. + // Therefore, we don't need it's instance. + new FilterStateManager( + globalStateStub, + () => { + return appStateStub; + }, + filterManager + ); + }); + + afterEach(() => { + if (subscription) { + subscription.unsubscribe(); + } + }); + + test('should update filter manager global filters', done => { + const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + globalStateStub.filters.push(f1); + + setTimeout(() => { + expect(filterManager.getGlobalFilters()).toHaveLength(1); + done(); + }, 100); + }); + + test('should update filter manager app filters', done => { + expect(filterManager.getAppFilters()).toHaveLength(0); + + const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34); + appStateStub.filters.push(f1); + + setTimeout(() => { + expect(filterManager.getAppFilters()).toHaveLength(1); + done(); + }, 100); + }); + + test('should update URL when filter manager filters are set', async () => { + appStateStub.save = sinon.stub(); + globalStateStub.save = sinon.stub(); + + const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34); + const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + + await filterManager.setFilters([f1, f2]); + + sinon.assert.calledOnce(appStateStub.save); + sinon.assert.calledOnce(globalStateStub.save); + }); + + test('should update URL when filter manager filters are added', async () => { + appStateStub.save = sinon.stub(); + globalStateStub.save = sinon.stub(); + + const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34); + const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); + + await filterManager.addFilters([f1, f2]); + + sinon.assert.calledOnce(appStateStub.save); + sinon.assert.calledOnce(globalStateStub.save); + }); +}); diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts new file mode 100644 index 0000000000000..d9c5af9a6c52f --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts @@ -0,0 +1,103 @@ +/* + * 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 { Filter, FilterStateStore } from '@kbn/es-query'; + +import _ from 'lodash'; +import { State } from 'ui/state_management/state'; +import { FilterManager } from './filter_manager'; + +/** + * FilterStateManager is responsible for watching for filter changes + * and synching with FilterManager, as well as syncing FilterManager changes + * back to the URL. + **/ +export class FilterStateManager { + filterManager: FilterManager; + globalState: State; + getAppState: () => State; + prevGlobalFilters: Filter[] | undefined; + prevAppFilters: Filter[] | undefined; + interval: NodeJS.Timeout | undefined; + + constructor(globalState: State, getAppState: () => State, filterManager: FilterManager) { + this.getAppState = getAppState; + this.globalState = globalState; + this.filterManager = filterManager; + + this.watchFilterState(); + + this.filterManager.getUpdates$().subscribe(() => { + this.updateAppState(); + }); + } + + destroy() { + if (this.interval) { + clearInterval(this.interval); + } + } + + private watchFilterState() { + // This is a temporary solution to remove rootscope. + // Moving forward, state should provide observable subscriptions. + this.interval = setInterval(() => { + const appState = this.getAppState(); + const stateUndefined = !appState || !this.globalState; + if (stateUndefined) return; + + const globalFilters = this.globalState.filters || []; + const appFilters = appState.filters || []; + + const globalFilterChanged = !( + this.prevGlobalFilters && _.isEqual(this.prevGlobalFilters, globalFilters) + ); + const appFilterChanged = !(this.prevAppFilters && _.isEqual(this.prevAppFilters, appFilters)); + const filterStateChanged = globalFilterChanged || appFilterChanged; + + if (!filterStateChanged) return; + + const newGlobalFilters = _.cloneDeep(globalFilters); + const newAppFilters = _.cloneDeep(appFilters); + FilterManager.setFiltersStore(newAppFilters, FilterStateStore.APP_STATE); + FilterManager.setFiltersStore(newGlobalFilters, FilterStateStore.GLOBAL_STATE); + + this.filterManager.setFilters(newGlobalFilters.concat(newAppFilters)); + + // store new filter changes + this.prevGlobalFilters = newGlobalFilters; + this.prevAppFilters = newAppFilters; + }, 10); + } + + private saveState() { + const appState = this.getAppState(); + if (appState) appState.save(); + this.globalState.save(); + } + + private updateAppState() { + // Update Angular state before saving State objects (which save it to URL) + const partitionedFilters = this.filterManager.getPartitionedFilters(); + const appState = this.getAppState(); + appState.filters = partitionedFilters.appFilters; + this.globalState.filters = partitionedFilters.globalFilters; + this.saveState(); + } +} diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts new file mode 100644 index 0000000000000..0e37e1ed4dec8 --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +export { FilterManager } from './filter_manager'; +export { FilterStateManager } from './filter_state_manager'; + +// @ts-ignore +export { uniqFilters } from './lib/uniq_filters'; diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/change_time_filter.test.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/change_time_filter.test.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/change_time_filter.test.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/change_time_filter.test.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/change_time_filter.test.mocks.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/change_time_filter.test.mocks.ts similarity index 92% rename from src/legacy/ui/public/filter_manager/lib/__tests__/change_time_filter.test.mocks.ts rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/change_time_filter.test.mocks.ts index 2cecfd0fe8b76..add054c492510 100644 --- a/src/legacy/ui/public/filter_manager/lib/__tests__/change_time_filter.test.mocks.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/change_time_filter.test.mocks.ts @@ -17,7 +17,7 @@ * under the License. */ -import { chromeServiceMock } from '../../../../../../core/public/mocks'; +import { chromeServiceMock } from '../../../../../../../../core/public/mocks'; jest.doMock('ui/new_platform', () => ({ npStart: { diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/dedup_filters.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/dedup_filters.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/dedup_filters.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/dedup_filters.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/extract_time_filter.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/extract_time_filter.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/extract_time_filter.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/extract_time_filter.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/generate_mapping_chain.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/generate_mapping_chain.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/generate_mapping_chain.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/generate_mapping_chain.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/map_and_flatten_filters.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_and_flatten_filters.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/map_and_flatten_filters.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_and_flatten_filters.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/map_default.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_default.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/map_default.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_default.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/map_exists.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_exists.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/map_exists.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_exists.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/map_filter.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_filter.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/map_filter.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_filter.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/map_geo_bounding_box.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_geo_bounding_box.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/map_geo_bounding_box.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_geo_bounding_box.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/map_geo_polygon.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_geo_polygon.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/map_geo_polygon.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_geo_polygon.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/map_match_all.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_match_all.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/map_match_all.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_match_all.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/map_missing.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_missing.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/map_missing.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_missing.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/map_phrase.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_phrase.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/map_phrase.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_phrase.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/map_query_string.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_query_string.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/map_query_string.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_query_string.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/map_range.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_range.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/map_range.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/map_range.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/only_disabled.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/only_disabled.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/only_disabled.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/only_disabled.js diff --git a/src/legacy/ui/public/filter_manager/lib/__tests__/uniq_filters.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/uniq_filters.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/__tests__/uniq_filters.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/__tests__/uniq_filters.js diff --git a/src/legacy/ui/public/filter_manager/lib/change_time_filter.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/change_time_filter.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/change_time_filter.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/change_time_filter.js diff --git a/src/legacy/ui/public/filter_manager/lib/compare_filters.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/compare_filters.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/compare_filters.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/compare_filters.js diff --git a/src/legacy/ui/public/filter_manager/lib/dedup_filters.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/dedup_filters.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/dedup_filters.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/dedup_filters.js diff --git a/src/legacy/ui/public/filter_manager/lib/extract_time_filter.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/extract_time_filter.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/extract_time_filter.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/extract_time_filter.js diff --git a/src/legacy/ui/public/filter_manager/lib/generate_mapping_chain.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/generate_mapping_chain.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/generate_mapping_chain.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/generate_mapping_chain.js diff --git a/src/legacy/ui/public/filter_manager/lib/map_and_flatten_filters.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_and_flatten_filters.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/map_and_flatten_filters.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_and_flatten_filters.js diff --git a/src/legacy/ui/public/filter_manager/lib/map_default.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_default.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/map_default.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_default.js diff --git a/src/legacy/ui/public/filter_manager/lib/map_exists.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_exists.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/map_exists.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_exists.js diff --git a/src/legacy/ui/public/filter_manager/lib/map_filter.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_filter.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/map_filter.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_filter.js diff --git a/src/legacy/ui/public/filter_manager/lib/map_geo_bounding_box.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_bounding_box.js similarity index 97% rename from src/legacy/ui/public/filter_manager/lib/map_geo_bounding_box.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_bounding_box.js index 89c3ccd96f95c..cba1344a53efb 100644 --- a/src/legacy/ui/public/filter_manager/lib/map_geo_bounding_box.js +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_bounding_box.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { SavedObjectNotFound } from '../../errors'; +import { SavedObjectNotFound } from 'ui/errors'; function getParams(filter, indexPattern) { const type = 'geo_bounding_box'; diff --git a/src/legacy/ui/public/filter_manager/lib/map_geo_polygon.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_polygon.js similarity index 97% rename from src/legacy/ui/public/filter_manager/lib/map_geo_polygon.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_polygon.js index 1ff1446d2ab72..913ca1d30a53f 100644 --- a/src/legacy/ui/public/filter_manager/lib/map_geo_polygon.js +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_polygon.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { SavedObjectNotFound } from '../../errors'; +import { SavedObjectNotFound } from 'ui/errors'; function getParams(filter, indexPattern) { const type = 'geo_polygon'; diff --git a/src/legacy/ui/public/filter_manager/lib/map_match_all.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_match_all.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/map_match_all.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_match_all.js diff --git a/src/legacy/ui/public/filter_manager/lib/map_missing.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_missing.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/map_missing.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_missing.js diff --git a/src/legacy/ui/public/filter_manager/lib/map_phrase.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrase.js similarity index 97% rename from src/legacy/ui/public/filter_manager/lib/map_phrase.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrase.js index 3082719ac983e..697c8250f7fed 100644 --- a/src/legacy/ui/public/filter_manager/lib/map_phrase.js +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrase.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { SavedObjectNotFound } from '../../errors'; +import { SavedObjectNotFound } from 'ui/errors'; function isScriptedPhrase(filter) { const value = _.get(filter, ['script', 'script', 'params', 'value']); diff --git a/src/legacy/ui/public/filter_manager/lib/map_phrases.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrases.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/map_phrases.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrases.js diff --git a/src/legacy/ui/public/filter_manager/lib/map_query_string.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_query_string.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/map_query_string.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_query_string.js diff --git a/src/legacy/ui/public/filter_manager/lib/map_range.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_range.js similarity index 96% rename from src/legacy/ui/public/filter_manager/lib/map_range.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_range.js index e413671ce4350..dabc6a4f9bd19 100644 --- a/src/legacy/ui/public/filter_manager/lib/map_range.js +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_range.js @@ -18,7 +18,7 @@ */ import { has, get } from 'lodash'; -import { SavedObjectNotFound } from '../../errors'; +import { SavedObjectNotFound } from 'ui/errors'; function isScriptedRange(filter) { @@ -43,7 +43,7 @@ function getParams(filter, indexPattern) { // external factors e.g. a reindex. We only need the index in order to grab the field formatter, so we fallback // on displaying the raw value if the index is invalid. let value = `${left} to ${right}`; - if (indexPattern) { + if (indexPattern && indexPattern.fields.byName[key]) { const convert = indexPattern.fields.byName[key].format.getConverterFor('text'); value = `${convert(left)} to ${convert(right)}`; } diff --git a/src/legacy/ui/public/filter_manager/lib/only_disabled.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_disabled.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/only_disabled.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_disabled.js diff --git a/src/legacy/ui/public/filter_manager/lib/only_state_changed.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_state_changed.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/only_state_changed.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/only_state_changed.js diff --git a/src/legacy/ui/public/filter_manager/lib/uniq_filters.js b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/uniq_filters.js similarity index 100% rename from src/legacy/ui/public/filter_manager/lib/uniq_filters.js rename to src/legacy/core_plugins/data/public/filter/filter_manager/lib/uniq_filters.js diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/partitioned_filters.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/partitioned_filters.ts new file mode 100644 index 0000000000000..e74b48b722cc4 --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/partitioned_filters.ts @@ -0,0 +1,25 @@ +/* + * 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 { Filter } from '@kbn/es-query'; + +export interface PartitionedFilters { + globalFilters: Filter[]; + appFilters: Filter[]; +} diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_filters_array.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_filters_array.ts new file mode 100644 index 0000000000000..27f627b477c35 --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_filters_array.ts @@ -0,0 +1,37 @@ +/* + * 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 { Filter } from '@kbn/es-query'; + +export function getFiltersArray(): Filter[] { + return [ + { + query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + { + query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + { + query: { match: { _type: { query: 'nginx', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + ]; +} diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts new file mode 100644 index 0000000000000..20d9e236f49be --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts @@ -0,0 +1,45 @@ +/* + * 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 { Filter, FilterStateStore } from '@kbn/es-query'; + +export function getFilter( + store: FilterStateStore, + disabled: boolean, + negated: boolean, + queryKey: string, + queryValue: any +): Filter { + return { + $state: { + store, + }, + meta: { + index: 'logstash-*', + disabled, + negate: negated, + alias: null, + }, + query: { + match: { + [queryKey]: queryValue, + }, + }, + }; +} diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_index_pattern.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_index_pattern.ts new file mode 100644 index 0000000000000..9a4f457fa9f91 --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_index_pattern.ts @@ -0,0 +1,28 @@ +/* + * 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. + */ + +export class StubIndexPatterns { + async get(index: any) { + return { + fields: { + byName: {}, + }, + }; + } +} diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts new file mode 100644 index 0000000000000..ab92016d1b9ab --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts @@ -0,0 +1,41 @@ +/* + * 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 sinon from 'sinon'; + +import { Filter } from '@kbn/es-query'; +import { State } from 'ui/state_management/state'; + +export class StubState implements State { + filters: Filter[]; + save: sinon.SinonSpy; + + constructor() { + this.save = sinon.stub(); + this.filters = []; + } + + getQueryParamName() { + return '_a'; + } + + translateHashToRison(stateHashOrRison: string | string[]): string | string[] { + return ''; + } +} diff --git a/src/legacy/core_plugins/data/public/filter/filter_service.ts b/src/legacy/core_plugins/data/public/filter/filter_service.ts index 0e1870ea598ac..2dbe8da3e2ad7 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_service.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_service.ts @@ -20,14 +20,23 @@ import { once } from 'lodash'; import { FilterBar, setupDirective as setupFilterBarDirective } from './filter_bar'; import { ApplyFiltersPopover, setupDirective as setupApplyFiltersDirective } from './apply_filters'; - +import { IndexPatterns } from '../index_patterns'; +import { FilterManager } from './filter_manager'; /** * FilterSearch Service * @internal */ + +export interface FilterServiceDependencies { + indexPatterns: IndexPatterns; +} + export class FilterService { - public setup() { + public setup({ indexPatterns }: FilterServiceDependencies) { + const filterManager = new FilterManager(indexPatterns); + return { + filterManager, ui: { ApplyFiltersPopover, FilterBar, diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index d762af70d2088..20be02510e61b 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -49,6 +49,7 @@ export class DataPlugin { // TODO: this is imported here to avoid circular imports. // eslint-disable-next-line @typescript-eslint/no-var-requires const { getInterpreter } = require('plugins/interpreter/interpreter'); + const indexPatternsService = this.indexPatterns.setup(); return { expressions: this.expressions.setup({ interpreter: { @@ -56,8 +57,10 @@ export class DataPlugin { renderersRegistry, }, }), - indexPatterns: this.indexPatterns.setup(), - filter: this.filter.setup(), + indexPatterns: indexPatternsService, + filter: this.filter.setup({ + indexPatterns: indexPatternsService.indexPatterns, + }), search: this.search.setup(), query: this.query.setup(), }; @@ -87,6 +90,7 @@ export { ExpressionRenderer, ExpressionRendererProps, ExpressionRunner } from '. /** @public types */ export { IndexPattern, StaticIndexPattern, StaticIndexPatternField, Field } from './index_patterns'; export { Query } from './query'; +export { FilterManager, FilterStateManager, uniqFilters } from './filter/filter_manager'; /** @public static code */ export { dateHistogramInterval } from '../common/date_histogram_interval'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/index.ts b/src/legacy/core_plugins/data/public/index_patterns/index.ts index 6f78399b30218..c7ff48407ff7c 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index.ts @@ -19,6 +19,7 @@ export { IndexPatternsService, + IndexPatterns, // types IndexPatternsSetup, IndexPattern, diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts index 539a21a22c841..8210ed8aad5f7 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts @@ -75,7 +75,7 @@ const ui = { IndexPatternSelect, }; -export { validateIndexPattern, constants, fixtures, ui }; +export { validateIndexPattern, constants, fixtures, ui, IndexPatterns }; /** @public */ export type IndexPatternsSetup = ReturnType; diff --git a/src/legacy/core_plugins/interpreter/common/types/index.ts b/src/legacy/core_plugins/interpreter/common/types/index.ts index a6ac385804468..bb8a554487f01 100644 --- a/src/legacy/core_plugins/interpreter/common/types/index.ts +++ b/src/legacy/core_plugins/interpreter/common/types/index.ts @@ -25,6 +25,7 @@ import { image } from './image'; import { nullType } from './null'; import { number } from './number'; import { pointseries } from './pointseries'; +import { range } from './range'; import { render } from './render'; import { shape } from './shape'; import { string } from './string'; @@ -41,6 +42,7 @@ export const typeSpecs = [ number, nullType, pointseries, + range, render, shape, string, @@ -59,3 +61,4 @@ export * from './kibana_datatable'; export * from './pointseries'; export * from './render'; export * from './style'; +export * from './range'; diff --git a/src/legacy/core_plugins/interpreter/common/types/range.ts b/src/legacy/core_plugins/interpreter/common/types/range.ts new file mode 100644 index 0000000000000..b08cc7b971de7 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/common/types/range.ts @@ -0,0 +1,51 @@ +/* + * 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 { ExpressionType, Render } from '../../types'; + +const name = 'range'; + +export interface Range { + type: typeof name; + from: number; + to: number; +} + +export const range = (): ExpressionType => ({ + name, + from: { + null: (): Range => { + return { + type: 'range', + from: 0, + to: 0, + }; + }, + }, + to: { + render: (value: Range): Render<{ text: string }> => { + const text = `from ${value.from} to ${value.to}`; + return { + type: 'render', + as: 'text', + value: { text }, + }; + }, + }, +}); diff --git a/src/legacy/core_plugins/interpreter/public/functions/index.js b/src/legacy/core_plugins/interpreter/public/functions/index.js index d87d4e7bf458c..38c3920f91bd2 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/index.js +++ b/src/legacy/core_plugins/interpreter/public/functions/index.js @@ -22,9 +22,10 @@ import { esaggs } from './esaggs'; import { font } from './font'; import { kibana } from './kibana'; import { kibanaContext } from './kibana_context'; +import { range } from './range'; import { visualization } from './visualization'; import { visDimension } from './vis_dimension'; export const functions = [ - clog, esaggs, font, kibana, kibanaContext, visualization, visDimension, + clog, esaggs, font, kibana, kibanaContext, range, visualization, visDimension, ]; diff --git a/src/legacy/core_plugins/interpreter/public/functions/range.ts b/src/legacy/core_plugins/interpreter/public/functions/range.ts new file mode 100644 index 0000000000000..622785dce4f94 --- /dev/null +++ b/src/legacy/core_plugins/interpreter/public/functions/range.ts @@ -0,0 +1,64 @@ +/* + * 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 { KibanaDatatable, Range } from '../../common/types'; +import { ExpressionFunction } from '../../types'; + +const name = 'range'; + +type Context = KibanaDatatable | null; + +interface Arguments { + from: number; + to: number; +} + +type Return = Range; // imported from type + +export const range = (): ExpressionFunction => ({ + name, + help: i18n.translate('interpreter.function.range.help', { + defaultMessage: 'Generates range object', + }), + type: 'range', + args: { + from: { + types: ['number'], + help: i18n.translate('interpreter.function.range.from.help', { + defaultMessage: 'Start of range', + }), + required: true, + }, + to: { + types: ['number'], + help: i18n.translate('interpreter.function.range.to.help', { + defaultMessage: 'End of range', + }), + required: true, + }, + }, + fn: (context, args) => { + return { + type: 'range', + from: args.from, + to: args.to, + }; + }, +}); diff --git a/src/legacy/core_plugins/kibana/public/context/index.js b/src/legacy/core_plugins/kibana/public/context/index.js index 5dfd6552771bb..04d01a9cb0d66 100644 --- a/src/legacy/core_plugins/kibana/public/context/index.js +++ b/src/legacy/core_plugins/kibana/public/context/index.js @@ -27,6 +27,7 @@ import './app'; import contextAppRouteTemplate from './index.html'; import { getRootBreadcrumbs } from '../discover/breadcrumbs'; import { npStart } from 'ui/new_platform'; +import { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; uiRoutes .when('/context/:indexPatternId/:type/:id*', { @@ -77,7 +78,7 @@ function ContextAppRouteController( 'contextAppRoute.state.successorCount', ], () => this.state.save(true)); - const updateSubsciption = queryFilter.getUpdates$().subscribe({ + const updateSubsciption = subscribeWithScope($scope, queryFilter.getUpdates$(), { next: () => { this.filters = _.cloneDeep(queryFilter.getFilters()); } diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_add_filter.js b/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_add_filter.js index e616fab9c0c2f..b4161e51bd979 100644 --- a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_add_filter.js +++ b/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_add_filter.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import sinon from 'sinon'; -import { FilterManagerProvider } from 'ui/filter_manager'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; @@ -35,8 +35,8 @@ describe('context app', function () { let addFilter; beforeEach(ngMock.inject(function createPrivateStubs(Private) { - filterManagerStub = createFilterManagerStub(); - Private.stub(FilterManagerProvider, filterManagerStub); + filterManagerStub = createQueryFilterStub(); + Private.stub(FilterBarQueryFilterProvider, filterManagerStub); addFilter = Private(QueryParameterActionsProvider).addFilter; })); @@ -46,11 +46,13 @@ describe('context app', function () { addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION'); - const filterManagerAddStub = filterManagerStub.add; + const filterManagerAddStub = filterManagerStub.addFilters; + //get the generated filter + const generatedFilter = filterManagerAddStub.firstCall.args[0][0]; + const queryKeys = Object.keys(generatedFilter.query.match); expect(filterManagerAddStub.calledOnce).to.be(true); - expect(filterManagerAddStub.firstCall.args[0]).to.eql('FIELD_NAME'); - expect(filterManagerAddStub.firstCall.args[1]).to.eql('FIELD_VALUE'); - expect(filterManagerAddStub.firstCall.args[2]).to.eql('FILTER_OPERATION'); + expect(queryKeys[0]).to.eql('FIELD_NAME'); + expect(generatedFilter.query.match[queryKeys[0]].query).to.eql('FIELD_VALUE'); }); it('should pass the index pattern id to the filterManager', function () { @@ -58,15 +60,18 @@ describe('context app', function () { addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION'); - const filterManagerAddStub = filterManagerStub.add; + const filterManagerAddStub = filterManagerStub.addFilters; + const generatedFilter = filterManagerAddStub.firstCall.args[0][0]; expect(filterManagerAddStub.calledOnce).to.be(true); - expect(filterManagerAddStub.firstCall.args[3]).to.eql('INDEX_PATTERN_ID'); + expect(generatedFilter.meta.index).to.eql('INDEX_PATTERN_ID'); }); }); }); -function createFilterManagerStub() { +function createQueryFilterStub() { return { - add: sinon.stub(), + addFilters: sinon.stub(), + invertFilter: sinon.stub(), + getAppFilters: sinon.stub(), }; } diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_predecessor_count.js b/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_predecessor_count.js index 156264cd98563..cd19f7af8d829 100644 --- a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_predecessor_count.js +++ b/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_predecessor_count.js @@ -20,8 +20,6 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { FilterManagerProvider } from 'ui/filter_manager'; - import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; @@ -33,8 +31,6 @@ describe('context app', function () { let increasePredecessorCount; beforeEach(ngMock.inject(function createPrivateStubs(Private) { - Private.stub(FilterManagerProvider, {}); - increasePredecessorCount = Private(QueryParameterActionsProvider).increasePredecessorCount; })); diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_successor_count.js b/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_successor_count.js index f4eb82d217c98..7036df1ea626f 100644 --- a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_successor_count.js +++ b/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_successor_count.js @@ -20,8 +20,6 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { FilterManagerProvider } from 'ui/filter_manager'; - import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; @@ -33,8 +31,6 @@ describe('context app', function () { let increaseSuccessorCount; beforeEach(ngMock.inject(function createPrivateStubs(Private) { - Private.stub(FilterManagerProvider, {}); - increaseSuccessorCount = Private(QueryParameterActionsProvider).increaseSuccessorCount; })); diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_predecessor_count.js b/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_predecessor_count.js index 0aa1f640f266d..7c1fa320ae17b 100644 --- a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_predecessor_count.js +++ b/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_predecessor_count.js @@ -20,8 +20,6 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { FilterManagerProvider } from 'ui/filter_manager'; - import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; @@ -33,8 +31,6 @@ describe('context app', function () { let setPredecessorCount; beforeEach(ngMock.inject(function createPrivateStubs(Private) { - Private.stub(FilterManagerProvider, {}); - setPredecessorCount = Private(QueryParameterActionsProvider).setPredecessorCount; })); diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_query_parameters.js b/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_query_parameters.js index 6a3638bc9f554..c64a1d40ea43c 100644 --- a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_query_parameters.js +++ b/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_query_parameters.js @@ -20,8 +20,6 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { FilterManagerProvider } from 'ui/filter_manager'; - import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; @@ -33,8 +31,6 @@ describe('context app', function () { let setQueryParameters; beforeEach(ngMock.inject(function createPrivateStubs(Private) { - Private.stub(FilterManagerProvider, {}); - setQueryParameters = Private(QueryParameterActionsProvider).setQueryParameters; })); diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_successor_count.js b/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_successor_count.js index f4e85eefe1e6f..d63bf2ecf53af 100644 --- a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_successor_count.js +++ b/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_successor_count.js @@ -20,8 +20,6 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { FilterManagerProvider } from 'ui/filter_manager'; - import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; @@ -33,8 +31,6 @@ describe('context app', function () { let setSuccessorCount; beforeEach(ngMock.inject(function createPrivateStubs(Private) { - Private.stub(FilterManagerProvider, {}); - setSuccessorCount = Private(QueryParameterActionsProvider).setSuccessorCount; })); diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/actions.js b/src/legacy/core_plugins/kibana/public/context/query_parameters/actions.js index fb314d3695bf2..cd5af188ad321 100644 --- a/src/legacy/core_plugins/kibana/public/context/query_parameters/actions.js +++ b/src/legacy/core_plugins/kibana/public/context/query_parameters/actions.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; -import { FilterManagerProvider } from 'ui/filter_manager'; +import { getFilterGenerator } from 'ui/filter_manager'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, @@ -30,7 +30,7 @@ import { export function QueryParameterActionsProvider(indexPatterns, Private) { const queryFilter = Private(FilterBarQueryFilterProvider); - const filterManager = Private(FilterManagerProvider); + const filterGen = getFilterGenerator(queryFilter); const setPredecessorCount = (state) => (predecessorCount) => ( state.queryParameters.predecessorCount = clamp( @@ -73,7 +73,8 @@ export function QueryParameterActionsProvider(indexPatterns, Private) { const addFilter = (state) => async (field, values, operation) => { const indexPatternId = state.queryParameters.indexPatternId; - filterManager.add(field, values, operation, indexPatternId); + const newFilters = filterGen.generate(field, values, operation, indexPatternId); + queryFilter.addFilters(newFilters); const indexPattern = await indexPatterns.get(indexPatternId); indexPattern.popularizeField(field.name, 1); }; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx index 473f2e0be4bb4..68a293de167f2 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx @@ -37,7 +37,8 @@ import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; import * as filterActions from 'plugins/kibana/discover/doc_table/actions/filter'; // @ts-ignore -import { FilterManagerProvider } from 'ui/filter_manager'; +import { getFilterGenerator } from 'ui/filter_manager'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { EmbeddableFactory } from 'ui/embeddable'; import { @@ -125,9 +126,10 @@ app.directive('dashboardApp', function($injector: IInjector) { const Private = $injector.get('Private'); - const filterManager = Private(FilterManagerProvider); + const queryFilter = Private(FilterBarQueryFilterProvider); + const filterGen = getFilterGenerator(queryFilter); const addFilter: AddFilterFn = ({ field, value, operator, index }, appState: TAppState) => { - filterActions.addFilter(field, value, operator, index, appState, filterManager); + filterActions.addFilter(field, value, operator, index, appState, filterGen); }; const indexPatterns = $injector.get<{ diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index e7afe807bbf5b..4e4bbde916c29 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -40,9 +40,9 @@ import { timefilter } from 'ui/timefilter'; import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; import { toastNotifications } from 'ui/notify'; import { VisProvider } from 'ui/vis'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; import { docTitle } from 'ui/doc_title'; -import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { intervalOptions } from 'ui/agg_types/buckets/_interval_options'; import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; import uiRoutes from 'ui/routes'; @@ -50,7 +50,8 @@ import { uiModules } from 'ui/modules'; import indexTemplate from '../index.html'; import { StateProvider } from 'ui/state_management/state'; import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; -import { FilterManagerProvider } from 'ui/filter_manager'; +import { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; +import { getFilterGenerator } from 'ui/filter_manager'; import { SavedObjectsClientProvider } from 'ui/saved_objects'; import { VisualizeLoaderProvider } from 'ui/visualize/loader/visualize_loader'; import { recentlyAccessed } from 'ui/persisted_log'; @@ -196,11 +197,13 @@ function discoverController( const visualizeLoader = Private(VisualizeLoaderProvider); let visualizeHandler; const Vis = Private(VisProvider); - const queryFilter = Private(FilterBarQueryFilterProvider); const responseHandler = vislibSeriesResponseHandlerProvider().handler; - const filterManager = Private(FilterManagerProvider); const getUnhashableStates = Private(getUnhashableStatesProvider); const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); + + const queryFilter = Private(FilterBarQueryFilterProvider); + const filterGen = getFilterGenerator(queryFilter); + const inspectorAdapters = { requests: new RequestAdapter() }; @@ -561,17 +564,19 @@ function discoverController( }); // update data source when filters update - filterUpdateSubscription = queryFilter.getUpdates$().subscribe( - () => { + filterUpdateSubscription = subscribeWithScope($scope, queryFilter.getUpdates$(), { + next: () => { $scope.filters = queryFilter.getFilters(); $scope.updateDataSource().then(function () { $state.save(); }); } - ); + }); // fetch data when filters fire fetch event - filterFetchSubscription = queryFilter.getFetches$().subscribe($scope.fetch); + filterFetchSubscription = subscribeWithScope($scope, queryFilter.getUpdates$(), { + next: $scope.fetch + }); // update data source when hitting forward/back and the query changes $scope.$listen($state, 'fetch_with_changes', function (diff) { @@ -860,7 +865,7 @@ function discoverController( // TODO: On array fields, negating does not negate the combination, rather all terms $scope.filterQuery = function (field, values, operation) { $scope.indexPattern.popularizeField(field, 1); - filterActions.addFilter(field, values, operation, $scope.indexPattern.id, $scope.state, filterManager); + filterActions.addFilter(field, values, operation, $scope.indexPattern.id, $scope.state, filterGen); }; $scope.addColumn = function addColumn(columnName) { diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/actions/filter.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/actions/filter.js index b65cba7d5c54c..d2bdec4fef5b0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/actions/filter.js +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/__tests__/actions/filter.js @@ -18,17 +18,22 @@ */ import { addFilter } from '../../actions/filter'; -import { FilterManagerProvider } from 'ui/filter_manager'; import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import NoDigestPromises from 'test_utils/no_digest_promises'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import sinon from 'sinon'; +function getFilterGeneratorStub() { + return { + add: sinon.stub() + }; +} + describe('doc table filter actions', function () { NoDigestPromises.activateForSuite(); - let filterManager; + let filterGen; let indexPattern; beforeEach(ngMock.module( @@ -41,8 +46,7 @@ describe('doc table filter actions', function () { beforeEach(ngMock.inject(function (Private) { indexPattern = Private(StubbedLogstashIndexPatternProvider); - filterManager = Private(FilterManagerProvider); - sinon.stub(filterManager, 'add'); + filterGen = getFilterGeneratorStub(); })); describe('add', function () { @@ -52,9 +56,9 @@ describe('doc table filter actions', function () { query: { query: 'foo', language: 'lucene' } }; const args = ['foo', ['bar'], '+', indexPattern, ]; - addFilter('foo', ['bar'], '+', indexPattern, state, filterManager); - expect(filterManager.add.calledOnce).to.be(true); - expect(filterManager.add.calledWith(...args)).to.be(true); + addFilter('foo', ['bar'], '+', indexPattern, state, filterGen); + expect(filterGen.add.calledOnce).to.be(true); + expect(filterGen.add.calledWith(...args)).to.be(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/actions/filter.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/actions/filter.js index 0156089148f4e..1a2854ec15412 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/actions/filter.js +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/actions/filter.js @@ -17,10 +17,10 @@ * under the License. */ -export function addFilter(field, values = [], operation, index, state, filterManager) { +export function addFilter(field, values = [], operation, index, state, filterGen) { if (!Array.isArray(values)) { values = [values]; } - filterManager.add(field, values, operation, index); + filterGen.add(field, values, operation, index); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index 6938437bdde33..8432268a4ee94 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -44,6 +44,7 @@ import { VisualizeConstants } from '../visualize_constants'; import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; import { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; +import { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; import { recentlyAccessed } from 'ui/persisted_log'; import { timefilter } from 'ui/timefilter'; import { getVisualizeLoader } from '../../../../../ui/public/visualize/loader'; @@ -415,12 +416,12 @@ function VisEditor( $scope.$listenAndDigestAsync(timefilter, 'refreshIntervalUpdate', updateRefreshInterval); // update the searchSource when filters update - const filterUpdateSubscription = queryFilter.getUpdates$().subscribe( - () => { + const filterUpdateSubscription = subscribeWithScope($scope, queryFilter.getUpdates$(), { + next: () => { $scope.filters = queryFilter.getFilters(); $scope.fetch(); - }, - ); + } + }); // update the searchSource when query updates $scope.fetch = function () { diff --git a/src/legacy/core_plugins/markdown_vis/public/markdown_fn.js b/src/legacy/core_plugins/markdown_vis/public/markdown_fn.js index c30047713d909..72324ddeb2eab 100644 --- a/src/legacy/core_plugins/markdown_vis/public/markdown_fn.js +++ b/src/legacy/core_plugins/markdown_vis/public/markdown_fn.js @@ -35,9 +35,12 @@ export const kibanaMarkdown = () => ({ aliases: ['_'], required: true, }, - fontSize: { - types: ['number'], - default: 12, + font: { + types: ['style'], + help: i18n.translate('markdownVis.function.font.help', { + defaultMessage: 'Font settings.' + }), + default: `{font size=12}`, }, openLinksInNewTab: { types: ['boolean'], @@ -51,7 +54,9 @@ export const kibanaMarkdown = () => ({ value: { visType: 'markdown', visConfig: { - ...args, + markdown: args.markdown, + openLinksInNewTab: args.openLinksInNewTab, + fontSize: parseInt(args.font.spec.fontSize), }, } }; diff --git a/src/legacy/core_plugins/markdown_vis/public/markdown_fn.test.js b/src/legacy/core_plugins/markdown_vis/public/markdown_fn.test.js index 18652aee65c20..2d4642636fa0c 100644 --- a/src/legacy/core_plugins/markdown_vis/public/markdown_fn.test.js +++ b/src/legacy/core_plugins/markdown_vis/public/markdown_fn.test.js @@ -23,7 +23,7 @@ import { kibanaMarkdown } from './markdown_fn'; describe('interpreter/functions#markdown', () => { const fn = functionWrapper(kibanaMarkdown); const args = { - fontSize: 12, + font: { spec: { fontSize: 12 } }, openLinksInNewTab: true, markdown: '## hello _markdown_', }; diff --git a/src/legacy/core_plugins/metric_vis/public/__snapshots__/metric_vis_fn.test.js.snap b/src/legacy/core_plugins/metric_vis/public/__snapshots__/metric_vis_fn.test.js.snap index e51555748dbcd..9aee51aa6c93d 100644 --- a/src/legacy/core_plugins/metric_vis/public/__snapshots__/metric_vis_fn.test.js.snap +++ b/src/legacy/core_plugins/metric_vis/public/__snapshots__/metric_vis_fn.test.js.snap @@ -9,42 +9,27 @@ Object { "listenOnChange": true, }, "visConfig": Object { - "addLegend": false, - "addTooltip": true, + "dimensions": Object { + "metrics": undefined, + }, "metric": Object { - "colorSchema": "Green to Red", - "colorsRange": Array [ - Object { - "from": 0, - "to": 10000, - }, - ], + "colorSchema": "\\"Green to Red\\"", + "colorsRange": undefined, "invertColors": false, "labels": Object { "show": true, }, - "metricColorMode": "None", - "metrics": Array [ - Object { - "accessor": 0, - "aggType": "count", - "format": Object { - "id": "number", - }, - "params": Object {}, - }, - ], + "metricColorMode": "\\"None\\"", "percentageMode": false, "style": Object { "bgColor": false, - "bgFill": "#000", + "bgFill": "\\"#000\\"", "fontSize": 60, "labelColor": false, - "subText": "", + "subText": "\\"\\"", }, "useRanges": false, }, - "type": "metric", }, "visData": Object { "columns": Array [ diff --git a/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.js b/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.js index dd35865a929ad..1ba28866d4e4b 100644 --- a/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.js +++ b/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.js @@ -19,9 +19,10 @@ import { functionsRegistry } from 'plugins/interpreter/registries'; import { i18n } from '@kbn/i18n'; +import { vislibColorMaps } from 'ui/vislib/components/color/colormaps'; export const metric = () => ({ - name: 'kibana_metric', + name: 'metricVis', type: 'render', context: { types: [ @@ -32,13 +33,110 @@ export const metric = () => ({ defaultMessage: 'Metric visualization' }), args: { - visConfig: { - types: ['string', 'null'], - default: '"{}"', + percentage: { + types: ['boolean'], + default: false, + help: i18n.translate('metricVis.function.percentage.help', { + defaultMessage: 'Shows metric in percentage mode. Requires colorRange to be set.' + }) + }, + colorScheme: { + types: ['string'], + default: '"Green to Red"', + options: Object.values(vislibColorMaps).map(value => value.id), + help: i18n.translate('metricVis.function.colorScheme.help', { + defaultMessage: 'Color scheme to use' + }) + }, + colorMode: { + types: ['string'], + default: '"None"', + options: ['None', 'Label', 'Background'], + help: i18n.translate('metricVis.function.colorMode.help', { + defaultMessage: 'Which part of metric to color' + }) + }, + colorRange: { + types: ['range'], + multi: true, + help: i18n.translate('metricVis.function.colorRange.help', { + defaultMessage: 'A range object specifying groups of values to which different colors should be applied.' + }) + }, + useRanges: { + types: ['boolean'], + default: false, + help: i18n.translate('metricVis.function.useRanges.help', { + defaultMessage: 'Enabled color ranges.' + }) + }, + invertColors: { + types: ['boolean'], + default: false, + help: i18n.translate('metricVis.function.invertColors.help', { + defaultMessage: 'Inverts the color ranges' + }) + }, + showLabels: { + types: ['boolean'], + default: true, + help: i18n.translate('metricVis.function.showLabels.help', { + defaultMessage: 'Shows labels under the metric values.' + }) + }, + bgFill: { + types: ['string'], + default: '"#000"', + aliases: ['backgroundFill', 'bgColor', 'backgroundColor'], + help: i18n.translate('metricVis.function.bgFill.help', { + defaultMessage: 'Color as html hex code (#123456), html color (red, blue) or rgba value (rgba(255,255,255,1)).' + }) + }, + font: { + types: ['style'], + help: i18n.translate('metricVis.function.font.help', { + defaultMessage: 'Font settings.' + }), + default: '{font size=60}', + }, + subText: { + types: ['string'], + aliases: ['label', 'text', 'description'], + default: '""', + help: i18n.translate('metricVis.function.subText.help', { + defaultMessage: 'Custom text to show under the metric' + }) + }, + metric: { + types: ['vis_dimension'], + help: i18n.translate('metricVis.function.metric.help', { + defaultMessage: 'metric dimension configuration' + }), + required: true, + multi: true, + }, + bucket: { + types: ['vis_dimension'], + help: i18n.translate('metricVis.function.bucket.help', { + defaultMessage: 'bucket dimension configuration' + }), }, }, fn(context, args) { - const visConfig = JSON.parse(args.visConfig); + + const dimensions = { + metrics: args.metric, + }; + + if (args.bucket) { + dimensions.bucket = args.bucket; + } + + if (args.percentage && (!args.colorRange || args.colorRange.length === 0)) { + throw new Error ('colorRange must be provided when using percentage'); + } + + const fontSize = parseInt(args.font.spec.fontSize); return { type: 'render', @@ -46,7 +144,27 @@ export const metric = () => ({ value: { visData: context, visType: 'metric', - visConfig, + visConfig: { + metric: { + percentageMode: args.percentage, + useRanges: args.useRanges, + colorSchema: args.colorScheme, + metricColorMode: args.colorMode, + colorsRange: args.colorRange, + labels: { + show: args.showLabels, + }, + invertColors: args.invertColors, + style: { + bgFill: args.bgFill, + bgColor: args.colorMode === 'Background', + labelColor: args.colorMode === 'Labels', + subText: args.subText, + fontSize, + } + }, + dimensions, + }, params: { listenOnChange: true, } diff --git a/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.test.js b/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.test.js index 9b393a33b75ff..bf98b55b39543 100644 --- a/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.test.js +++ b/src/legacy/core_plugins/metric_vis/public/metric_vis_fn.test.js @@ -27,47 +27,43 @@ describe('interpreter/functions#metric', () => { rows: [{ 'col-0-1': 0 }], columns: [{ id: 'col-0-1', name: 'Count' }], }; - const visConfig = { - addTooltip: true, - addLegend: false, - type: 'metric', - metric: { - percentageMode: false, - useRanges: false, - colorSchema: 'Green to Red', - metricColorMode: 'None', - colorsRange: [ - { - from: 0, - to: 10000, - } - ], - labels: { - show: true, - }, - invertColors: false, - style: { - bgFill: '#000', - bgColor: false, - labelColor: false, - subText: '', - fontSize: 60, - }, - metrics: [ - { - accessor: 0, - format: { - id: 'number' - }, - params: {}, - aggType: 'count', - } - ] + const args = { + percentageMode: false, + useRanges: false, + colorSchema: 'Green to Red', + metricColorMode: 'None', + colorsRange: [ + { + from: 0, + to: 10000, + } + ], + labels: { + show: true, }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 60, + }, + font: { spec: { fontSize: 60 } }, + metrics: [ + { + accessor: 0, + format: { + id: 'number' + }, + params: {}, + aggType: 'count', + } + ] }; it('returns an object with the correct structure', () => { - const actual = fn(context, { visConfig: JSON.stringify(visConfig) }); + const actual = fn(context, args); expect(actual).toMatchSnapshot(); }); }); diff --git a/src/legacy/ui/public/chrome/__mocks__/index.js b/src/legacy/ui/public/chrome/__mocks__/index.js index c981461d30f2f..ca603845eaf86 100644 --- a/src/legacy/ui/public/chrome/__mocks__/index.js +++ b/src/legacy/ui/public/chrome/__mocks__/index.js @@ -31,6 +31,7 @@ const chrome = { addBasePath: path => path, getInjected: jest.fn(), getUiSettingsClient: () => uiSettingsClient, + getSavedObjectsClient: () => '', getXsrfToken: () => 'kbn-xsrf-token', }; diff --git a/src/legacy/ui/public/errors.js b/src/legacy/ui/public/errors.js index a6dce456b4d8d..b3fb08a78b61d 100644 --- a/src/legacy/ui/public/errors.js +++ b/src/legacy/ui/public/errors.js @@ -49,7 +49,12 @@ export class KbnError { // http://stackoverflow.com/questions/33870684/why-doesnt-instanceof-work-on-instances-of-error-subclasses-under-babel-node // Hence we are inheriting from it this way, instead of using extends Error, and this will then preserve // instanceof checks. -createLegacyClass(KbnError).inherits(Error); +try { + createLegacyClass(KbnError).inherits(Error); +} catch (e) { + // Avoid TypeError: Cannot redefine property: prototype +} + /** * Request Failure - When an entire multi request fails diff --git a/src/legacy/ui/public/filter_manager/__tests__/_add_filters.js b/src/legacy/ui/public/filter_manager/__tests__/_add_filters.js deleted file mode 100644 index 300f5d9520721..0000000000000 --- a/src/legacy/ui/public/filter_manager/__tests__/_add_filters.js +++ /dev/null @@ -1,211 +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 _ from 'lodash'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import MockState from 'fixtures/mock_state'; -import { FilterBarQueryFilterProvider } from '../query_filter'; - -describe('add filters', function () { - require('test_utils/no_digest_promises').activateForSuite(); - - let filters; - let queryFilter; - let $rootScope; - let appState; - let globalState; - - beforeEach(ngMock.module( - 'kibana', - 'kibana/courier', - 'kibana/global_state', - function ($provide) { - $provide.service('indexPatterns', require('fixtures/mock_index_patterns')); - - appState = new MockState({ filters: [] }); - $provide.service('getAppState', function () { - return function () { return appState; }; - }); - - globalState = new MockState({ filters: [] }); - $provide.service('globalState', function () { - return globalState; - }); - } - )); - - beforeEach(ngMock.inject(function (_$rootScope_, Private) { - $rootScope = _$rootScope_; - queryFilter = Private(FilterBarQueryFilterProvider); - })); - - beforeEach(function () { - filters = [ - { - query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, - meta: { index: 'logstash-*', negate: false, disabled: false } - }, - { - query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, - meta: { index: 'logstash-*', negate: false, disabled: false } - }, - { - query: { match: { '_type': { query: 'nginx', type: 'phrase' } } }, - meta: { index: 'logstash-*', negate: false, disabled: false } - } - ]; - }); - - describe('adding filters', function () { - it('should add filters to appState', async function () { - await queryFilter.addFilters(filters); - expect(appState.filters.length).to.be(3); - expect(globalState.filters.length).to.be(0); - }); - - it('should add filters to globalState', async function () { - await queryFilter.addFilters(filters, true); - - expect(appState.filters.length).to.be(0); - expect(globalState.filters.length).to.be(3); - }); - - it('should accept a single filter', async function () { - await queryFilter.addFilters(filters[0]); - - expect(appState.filters.length).to.be(1); - expect(globalState.filters.length).to.be(0); - }); - - it('should allow overwriting a positive filter by a negated one', async function () { - - // Add negate: false version of the filter - const filter = _.cloneDeep(filters[0]); - filter.meta.negate = false; - - await queryFilter.addFilters(filter); - $rootScope.$digest(); - expect(appState.filters.length).to.be(1); - expect(appState.filters[0]).to.eql(filter); - - // Add negate: true version of the same filter - const negatedFilter = _.cloneDeep(filters[0]); - negatedFilter.meta.negate = true; - - await queryFilter.addFilters(negatedFilter); - $rootScope.$digest(); - // The negated filter should overwrite the positive one - expect(appState.filters.length).to.be(1); - expect(appState.filters[0]).to.eql(negatedFilter); - }); - - it('should allow overwriting a negated filter by a positive one', async function () { - // Add negate: true version of the same filter - const negatedFilter = _.cloneDeep(filters[0]); - negatedFilter.meta.negate = true; - - await queryFilter.addFilters(negatedFilter); - $rootScope.$digest(); - - // The negated filter should overwrite the positive one - expect(appState.filters.length).to.be(1); - expect(appState.filters[0]).to.eql(negatedFilter); - - // Add negate: false version of the filter - const filter = _.cloneDeep(filters[0]); - filter.meta.negate = false; - - await queryFilter.addFilters(filter); - $rootScope.$digest(); - expect(appState.filters.length).to.be(1); - expect(appState.filters[0]).to.eql(filter); - }); - - it('should fire the update and fetch events', async function () { - const updateStub = sinon.stub(); - const fetchStub = sinon.stub(); - - queryFilter.getUpdates$().subscribe({ - next: updateStub, - }); - - queryFilter.getFetches$().subscribe({ - next: fetchStub, - }); - - // set up the watchers, add new filters, and crank the digest loop - $rootScope.$digest(); - await queryFilter.addFilters(filters); - $rootScope.$digest(); - - // updates should trigger state saves - expect(appState.save.callCount).to.be(1); - expect(globalState.save.callCount).to.be(1); - - // this time, events should be emitted - expect(fetchStub.called); - expect(updateStub.called); - }); - }); - - describe('filter reconciliation', function () { - it('should de-dupe appState filters being added', async function () { - const newFilter = _.cloneDeep(filters[1]); - appState.filters = filters; - $rootScope.$digest(); - expect(appState.filters.length).to.be(3); - - await queryFilter.addFilters(newFilter); - $rootScope.$digest(); - expect(appState.filters.length).to.be(3); - }); - - it('should de-dupe globalState filters being added', async function () { - const newFilter = _.cloneDeep(filters[1]); - globalState.filters = filters; - $rootScope.$digest(); - expect(globalState.filters.length).to.be(3); - - await queryFilter.addFilters(newFilter, true); - $rootScope.$digest(); - expect(globalState.filters.length).to.be(3); - }); - - it('should mutate global filters on appState filter changes', async function () { - const idx = 1; - globalState.filters = filters; - $rootScope.$digest(); - - const appFilter = _.cloneDeep(filters[idx]); - appFilter.meta.negate = true; - await queryFilter.addFilters(appFilter); - $rootScope.$digest(); - - const res = queryFilter.getFilters(); - expect(res).to.have.length(3); - _.each(res, function (filter, i) { - expect(filter.$state.store).to.be('globalState'); - // make sure global filter actually mutated - expect(filter.meta.negate).to.be(i === idx); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/filter_manager/__tests__/_get_filters.js b/src/legacy/ui/public/filter_manager/__tests__/_get_filters.js deleted file mode 100644 index 3805ce1ccaf1e..0000000000000 --- a/src/legacy/ui/public/filter_manager/__tests__/_get_filters.js +++ /dev/null @@ -1,202 +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 _ from 'lodash'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import MockState from 'fixtures/mock_state'; -import { FilterBarQueryFilterProvider } from '../query_filter'; - -describe('get filters', function () { - const storeNames = { - app: 'appState', - global: 'globalState' - }; - let queryFilter; - let appState; - let globalState; - - beforeEach(ngMock.module( - 'kibana', - 'kibana/global_state', - function ($provide) { - appState = new MockState({ filters: [] }); - $provide.service('getAppState', function () { - return function () { return appState; }; - }); - - globalState = new MockState({ filters: [] }); - $provide.service('globalState', function () { - return globalState; - }); - } - )); - - beforeEach(ngMock.inject(function (_$rootScope_, Private) { - queryFilter = Private(FilterBarQueryFilterProvider); - })); - - describe('getFilters method', function () { - let filters; - - beforeEach(function () { - filters = [ - { query: { match: { extension: { query: 'jpg', type: 'phrase' } } } }, - { query: { match: { '@tags': { query: 'info', type: 'phrase' } } } }, - null - ]; - }); - - it('should return app and global filters', function () { - appState.filters = [filters[0]]; - globalState.filters = [filters[1]]; - - // global filters should be listed first - let res = queryFilter.getFilters(); - expect(res[0]).to.eql(filters[1]); - expect(res[1]).to.eql(filters[0]); - - // should return updated version of filters - const newFilter = { query: { match: { '_type': { query: 'nginx', type: 'phrase' } } } }; - appState.filters.push(newFilter); - - res = queryFilter.getFilters(); - expect(res).to.contain(newFilter); - }); - - it('should append the state store', function () { - appState.filters = [filters[0]]; - globalState.filters = [filters[1]]; - - const res = queryFilter.getFilters(); - expect(res[0].$state.store).to.be(storeNames.global); - expect(res[1].$state.store).to.be(storeNames.app); - }); - - it('should return non-null filters from specific states', function () { - const states = [ - [ globalState, queryFilter.getGlobalFilters ], - [ appState, queryFilter.getAppFilters ], - ]; - - _.each(states, function (state) { - state[0].filters = filters.slice(0); - expect(state[0].filters).to.contain(null); - - const res = state[1](); - expect(res.length).to.be(state[0].filters.length); - expect(state[0].filters).to.not.contain(null); - }); - }); - - it('should replace the state, not save it', function () { - const states = [ - [ globalState, queryFilter.getGlobalFilters ], - [ appState, queryFilter.getAppFilters ], - ]; - - expect(appState.save.called).to.be(false); - expect(appState.replace.called).to.be(false); - - - _.each(states, function (state) { - expect(state[0].save.called).to.be(false); - expect(state[0].replace.called).to.be(false); - - state[0].filters = filters.slice(0); - state[1](); - expect(state[0].save.called).to.be(false); - expect(state[0].replace.called).to.be(true); - }); - }); - }); - - describe('filter reconciliation', function () { - let filters; - - beforeEach(function () { - filters = [ - { - query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - }, - { - query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - }, - { - query: { match: { '_type': { query: 'nginx', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - } - ]; - }); - - it('should skip appState filters that match globalState filters', function () { - globalState.filters = filters; - const appFilter = _.cloneDeep(filters[1]); - appState.filters.push(appFilter); - - // global filters should be listed first - const res = queryFilter.getFilters(); - expect(res).to.have.length(3); - _.each(res, function (filter) { - expect(filter.$state.store).to.be('globalState'); - }); - }); - - it('should append conflicting appState filters', function () { - globalState.filters = filters; - const appFilter = _.cloneDeep(filters[1]); - appFilter.meta.negate = true; - appState.filters.push(appFilter); - - // global filters should be listed first - const res = queryFilter.getFilters(); - expect(res).to.have.length(4); - expect(res.filter(function (filter) { - return filter.$state.store === storeNames.global; - }).length).to.be(3); - expect(res.filter(function (filter) { - return filter.$state.store === storeNames.app; - }).length).to.be(1); - }); - - it('should not affect disabled filters', function () { - // test adding to globalState - globalState.filters = _.map(filters, function (filter) { - const f = _.cloneDeep(filter); - f.meta.disabled = true; - return f; - }); - _.each(filters, function (filter) { globalState.filters.push(filter); }); - let res = queryFilter.getFilters(); - expect(res).to.have.length(6); - - // test adding to appState - globalState.filters = _.map(filters, function (filter) { - const f = _.cloneDeep(filter); - f.meta.disabled = true; - return f; - }); - _.each(filters, function (filter) { appState.filters.push(filter); }); - res = queryFilter.getFilters(); - expect(res).to.have.length(6); - }); - }); -}); diff --git a/src/legacy/ui/public/filter_manager/__tests__/_invert_filters.js b/src/legacy/ui/public/filter_manager/__tests__/_invert_filters.js deleted file mode 100644 index de7222dca7daf..0000000000000 --- a/src/legacy/ui/public/filter_manager/__tests__/_invert_filters.js +++ /dev/null @@ -1,147 +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 _ from 'lodash'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import MockState from 'fixtures/mock_state'; -import { FilterBarQueryFilterProvider } from '../query_filter'; - -describe('invert filters', function () { - let filters; - let queryFilter; - let $rootScope; - let appState; - let globalState; - - beforeEach(ngMock.module( - 'kibana', - 'kibana/courier', - 'kibana/global_state', - function ($provide) { - $provide.service('indexPatterns', require('fixtures/mock_index_patterns')); - - appState = new MockState({ filters: [] }); - $provide.service('getAppState', function () { - return function () { return appState; }; - }); - - globalState = new MockState({ filters: [] }); - $provide.service('globalState', function () { - return globalState; - }); - } - )); - - beforeEach(ngMock.inject(function (_$rootScope_, Private) { - $rootScope = _$rootScope_; - queryFilter = Private(FilterBarQueryFilterProvider); - - filters = [ - { - query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - }, - { - query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - }, - { - query: { match: { '_type': { query: 'nginx', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - } - ]; - })); - - describe('inverting a filter', function () { - it('should swap the negate property in appState', function () { - _.each(filters, function (filter) { - expect(filter.meta.negate).to.be(false); - appState.filters.push(filter); - }); - - queryFilter.invertFilter(filters[1]); - expect(appState.filters[1].meta.negate).to.be(true); - }); - - it('should toggle the negate property in globalState', function () { - _.each(filters, function (filter) { - expect(filter.meta.negate).to.be(false); - globalState.filters.push(filter); - }); - - queryFilter.invertFilter(filters[1]); - expect(globalState.filters[1].meta.negate).to.be(true); - }); - - it('should fire the update and fetch events', function () { - const updateStub = sinon.stub(); - const fetchStub = sinon.stub(); - - queryFilter.getUpdates$().subscribe({ - next: updateStub, - }); - - queryFilter.getFetches$().subscribe({ - next: fetchStub, - }); - appState.filters = filters; - - // set up the watchers - $rootScope.$digest(); - queryFilter.invertFilter(filters[1]); - // trigger the digest loop to fire the watchers - $rootScope.$digest(); - - expect(fetchStub.called); - expect(updateStub.called); - }); - }); - - describe('bulk inverting', function () { - beforeEach(function () { - appState.filters = filters; - globalState.filters = _.map(_.cloneDeep(filters), function (filter) { - filter.meta.negate = true; - return filter; - }); - }); - - it('should swap the negate state for all filters', function () { - queryFilter.invertAll(); - _.each(appState.filters, function (filter) { - expect(filter.meta.negate).to.be(true); - }); - _.each(globalState.filters, function (filter) { - expect(filter.meta.negate).to.be(false); - }); - }); - - it('should work without global state filters', function () { - // remove global filters - delete globalState.filters; - - queryFilter.invertAll(); - _.each(appState.filters, function (filter) { - expect(filter.meta.negate).to.be(true); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/filter_manager/__tests__/_pin_filters.js b/src/legacy/ui/public/filter_manager/__tests__/_pin_filters.js deleted file mode 100644 index 8d292bfb28d3a..0000000000000 --- a/src/legacy/ui/public/filter_manager/__tests__/_pin_filters.js +++ /dev/null @@ -1,183 +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 _ from 'lodash'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import MockState from 'fixtures/mock_state'; -import { FilterBarQueryFilterProvider } from '../query_filter'; - -describe('pin filters', function () { - let filters; - let queryFilter; - let $rootScope; - let appState; - let globalState; - - beforeEach(ngMock.module( - 'kibana', - 'kibana/courier', - 'kibana/global_state', - function ($provide) { - $provide.service('indexPatterns', require('fixtures/mock_index_patterns')); - - appState = new MockState({ filters: [] }); - $provide.service('getAppState', function () { - return function () { return appState; }; - }); - - globalState = new MockState({ filters: [] }); - $provide.service('globalState', function () { - return globalState; - }); - } - )); - - beforeEach(ngMock.inject(function (_$rootScope_, Private) { - $rootScope = _$rootScope_; - queryFilter = Private(FilterBarQueryFilterProvider); - - filters = [ - { - query: { match: { extension: { query: 'gif', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - }, - { - query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, - meta: { negate: true, disabled: false } - }, - { - query: { match: { extension: { query: 'png', type: 'phrase' } } }, - meta: { negate: true, disabled: true } - }, - { - query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - }, - { - query: { match: { '@tags': { query: 'success', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - }, - { - query: { match: { '@tags': { query: 'security', type: 'phrase' } } }, - meta: { negate: true, disabled: false } - }, - { - query: { match: { '_type': { query: 'nginx', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - }, - { - query: { match: { '_type': { query: 'apache', type: 'phrase' } } }, - meta: { negate: true, disabled: true } - } - ]; - })); - - describe('pin a filter', function () { - beforeEach(function () { - globalState.filters = _.filter(filters, function (filter) { - return !!filter.query.match._type; - }); - appState.filters = _.filter(filters, function (filter) { - return !filter.query.match._type; - }); - expect(globalState.filters).to.have.length(2); - expect(appState.filters).to.have.length(6); - }); - - it('should move filter from appState to globalState', function () { - const filter = appState.filters[1]; - - queryFilter.pinFilter(filter); - expect(globalState.filters).to.contain(filter); - expect(globalState.filters).to.have.length(3); - expect(appState.filters).to.have.length(5); - }); - - it('should move filter from globalState to appState', function () { - const filter = globalState.filters[1]; - - queryFilter.pinFilter(filter); - expect(appState.filters).to.contain(filter); - expect(globalState.filters).to.have.length(1); - expect(appState.filters).to.have.length(7); - }); - - - it('should only fire the update event', function () { - const updateStub = sinon.stub(); - const fetchStub = sinon.stub(); - - queryFilter.getUpdates$().subscribe({ - next: updateStub, - }); - - queryFilter.getFetches$().subscribe({ - next: fetchStub, - }); - - const filter = appState.filters[1]; - $rootScope.$digest(); - - queryFilter.pinFilter(filter); - $rootScope.$digest(); - - expect(!fetchStub.called); - expect(updateStub.called); - }); - }); - - describe('bulk pinning', function () { - beforeEach(function () { - globalState.filters = _.filter(filters, function (filter) { - return !!filter.query.match.extension; - }); - appState.filters = _.filter(filters, function (filter) { - return !filter.query.match.extension; - }); - expect(globalState.filters).to.have.length(3); - expect(appState.filters).to.have.length(5); - }); - - it('should swap the filters in both states', function () { - const appSample = _.sample(appState.filters); - const globalSample = _.sample(globalState.filters); - - queryFilter.pinAll(); - expect(globalState.filters).to.have.length(5); - expect(appState.filters).to.have.length(3); - - expect(globalState.filters).to.contain(appSample); - expect(appState.filters).to.contain(globalSample); - }); - - it('should move all filters to globalState', function () { - queryFilter.pinAll(true); - expect(globalState.filters).to.have.length(8); - expect(appState.filters).to.have.length(0); - }); - - it('should move all filters to appState', function () { - queryFilter.pinAll(false); - expect(globalState.filters).to.have.length(0); - expect(appState.filters).to.have.length(8); - }); - }); -}); diff --git a/src/legacy/ui/public/filter_manager/__tests__/_remove_filters.js b/src/legacy/ui/public/filter_manager/__tests__/_remove_filters.js deleted file mode 100644 index 2b9e31aa794eb..0000000000000 --- a/src/legacy/ui/public/filter_manager/__tests__/_remove_filters.js +++ /dev/null @@ -1,170 +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 _ from 'lodash'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import MockState from 'fixtures/mock_state'; -import { FilterBarQueryFilterProvider } from '../query_filter'; - -describe('remove filters', function () { - let filters; - let queryFilter; - let $rootScope; - let appState; - let globalState; - - beforeEach(ngMock.module( - 'kibana', - 'kibana/courier', - 'kibana/global_state', - function ($provide) { - $provide.service('indexPatterns', require('fixtures/mock_index_patterns')); - - appState = new MockState({ filters: [] }); - $provide.service('getAppState', function () { - return function () { return appState; }; - }); - - globalState = new MockState({ filters: [] }); - $provide.service('globalState', function () { - return globalState; - }); - } - )); - - beforeEach(ngMock.inject(function (_$rootScope_, Private) { - $rootScope = _$rootScope_; - queryFilter = Private(FilterBarQueryFilterProvider); - filters = [ - { - query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - }, - { - query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - }, - { - query: { match: { '_type': { query: 'nginx', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - } - ]; - })); - - describe('removing a filter', function () { - it('should remove the filter from appState', function () { - appState.filters = filters; - expect(appState.filters).to.have.length(3); - queryFilter.removeFilter(filters[0]); - expect(appState.filters).to.have.length(2); - }); - - it('should remove the filter from globalState', function () { - globalState.filters = filters; - expect(globalState.filters).to.have.length(3); - queryFilter.removeFilter(filters[0]); - expect(globalState.filters).to.have.length(2); - }); - - it('should fire the update and fetch events', function () { - const updateStub = sinon.stub(); - const fetchStub = sinon.stub(); - - queryFilter.getUpdates$().subscribe({ - next: updateStub, - }); - - queryFilter.getFetches$().subscribe({ - next: fetchStub, - }); - - appState.filters = filters; - $rootScope.$digest(); - - queryFilter.removeFilter(filters[0]); - $rootScope.$digest(); - - // this time, events should be emitted - expect(fetchStub.called); - expect(updateStub.called); - }); - - it('should remove matching filters', function () { - globalState.filters.push(filters[0]); - globalState.filters.push(filters[1]); - appState.filters.push(filters[2]); - $rootScope.$digest(); - - queryFilter.removeFilter(filters[0]); - $rootScope.$digest(); - expect(globalState.filters).to.have.length(1); - expect(appState.filters).to.have.length(1); - }); - - it('should remove matching filters by comparison', function () { - globalState.filters.push(filters[0]); - globalState.filters.push(filters[1]); - appState.filters.push(filters[2]); - $rootScope.$digest(); - - queryFilter.removeFilter(_.cloneDeep(filters[0])); - $rootScope.$digest(); - expect(globalState.filters).to.have.length(1); - expect(appState.filters).to.have.length(1); - - queryFilter.removeFilter(_.cloneDeep(filters[2])); - $rootScope.$digest(); - expect(globalState.filters).to.have.length(1); - expect(appState.filters).to.have.length(0); - }); - - it('should do nothing with a non-matching filter', function () { - globalState.filters.push(filters[0]); - globalState.filters.push(filters[1]); - appState.filters.push(filters[2]); - $rootScope.$digest(); - - const missedFilter = _.cloneDeep(filters[0]); - missedFilter.meta = { - negate: !filters[0].meta.negate - }; - - queryFilter.removeFilter(missedFilter); - $rootScope.$digest(); - expect(globalState.filters).to.have.length(2); - expect(appState.filters).to.have.length(1); - }); - }); - - describe('bulk removal', function () { - it('should remove all the filters from both states', function () { - globalState.filters.push(filters[0]); - globalState.filters.push(filters[1]); - appState.filters.push(filters[2]); - expect(globalState.filters).to.have.length(2); - expect(appState.filters).to.have.length(1); - - queryFilter.removeAll(); - expect(globalState.filters).to.have.length(0); - expect(appState.filters).to.have.length(0); - }); - }); -}); diff --git a/src/legacy/ui/public/filter_manager/__tests__/_toggle_filters.js b/src/legacy/ui/public/filter_manager/__tests__/_toggle_filters.js deleted file mode 100644 index bd7781098259d..0000000000000 --- a/src/legacy/ui/public/filter_manager/__tests__/_toggle_filters.js +++ /dev/null @@ -1,204 +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 _ from 'lodash'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import sinon from 'sinon'; -import MockState from 'fixtures/mock_state'; -import { FilterBarQueryFilterProvider } from '../query_filter'; - -describe('toggle filters', function () { - let filters; - let queryFilter; - let $rootScope; - let appState; - let globalState; - - beforeEach(ngMock.module( - 'kibana', - 'kibana/courier', - 'kibana/global_state', - function ($provide) { - $provide.service('indexPatterns', require('fixtures/mock_index_patterns')); - - appState = new MockState({ filters: [] }); - $provide.service('getAppState', function () { - return function () { return appState; }; - }); - - globalState = new MockState({ filters: [] }); - $provide.service('globalState', function () { - return globalState; - }); - } - )); - - beforeEach(ngMock.inject(function (_$rootScope_, Private) { - $rootScope = _$rootScope_; - queryFilter = Private(FilterBarQueryFilterProvider); - filters = [ - { - query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - }, - { - query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - }, - { - query: { match: { '_type': { query: 'nginx', type: 'phrase' } } }, - meta: { negate: false, disabled: false } - } - ]; - })); - - describe('toggling a filter', function () { - it('should toggle the disabled property in appState', function () { - _.each(filters, function (filter) { - expect(filter.meta.disabled).to.be(false); - appState.filters.push(filter); - }); - - queryFilter.toggleFilter(filters[1]); - expect(appState.filters[1].meta.disabled).to.be(true); - }); - - it('should toggle the disabled property in globalState', function () { - _.each(filters, function (filter) { - expect(filter.meta.disabled).to.be(false); - globalState.filters.push(filter); - }); - - queryFilter.toggleFilter(filters[1]); - expect(globalState.filters[1].meta.disabled).to.be(true); - }); - - it('should fire the update and fetch events', function () { - const updateStub = sinon.stub(); - const fetchStub = sinon.stub(); - - queryFilter.getUpdates$().subscribe({ - next: updateStub, - }); - - queryFilter.getFetches$().subscribe({ - next: fetchStub, - }); - - appState.filters = filters; - $rootScope.$digest(); - - queryFilter.toggleFilter(filters[1]); - $rootScope.$digest(); - - // this time, events should be emitted - expect(fetchStub.called); - expect(updateStub.called); - }); - - it('should always enable the filter', function () { - appState.filters = filters.map(function (filter) { - filter.meta.disabled = true; - return filter; - }); - - expect(appState.filters[1].meta.disabled).to.be(true); - queryFilter.toggleFilter(filters[1], false); - expect(appState.filters[1].meta.disabled).to.be(false); - queryFilter.toggleFilter(filters[1], false); - expect(appState.filters[1].meta.disabled).to.be(false); - }); - - it('should always disable the filter', function () { - globalState.filters = filters; - - expect(globalState.filters[1].meta.disabled).to.be(false); - queryFilter.toggleFilter(filters[1], true); - expect(globalState.filters[1].meta.disabled).to.be(true); - queryFilter.toggleFilter(filters[1], true); - expect(globalState.filters[1].meta.disabled).to.be(true); - }); - - it('should work without appState', function () { - appState = undefined; - globalState.filters = filters; - - expect(globalState.filters[1].meta.disabled).to.be(false); - expect(queryFilter.getFilters()).to.have.length(3); - queryFilter.toggleFilter(filters[1]); - expect(globalState.filters[1].meta.disabled).to.be(true); - }); - }); - - describe('bulk toggling', function () { - beforeEach(function () { - appState.filters = filters; - globalState.filters = _.map(_.cloneDeep(filters), function (filter) { - filter.meta.disabled = true; - return filter; - }); - }); - - it('should swap the enabled state for all filters', function () { - queryFilter.toggleAll(); - _.each(appState.filters, function (filter) { - expect(filter.meta.disabled).to.be(true); - }); - _.each(globalState.filters, function (filter) { - expect(filter.meta.disabled).to.be(false); - }); - }); - - it('should enable all filters', function () { - queryFilter.toggleAll(true); - _.each(appState.filters, function (filter) { - expect(filter.meta.disabled).to.be(true); - }); - _.each(globalState.filters, function (filter) { - expect(filter.meta.disabled).to.be(true); - }); - }); - - it('should disable all filters', function () { - queryFilter.toggleAll(false); - _.each(appState.filters, function (filter) { - expect(filter.meta.disabled).to.be(false); - }); - _.each(globalState.filters, function (filter) { - expect(filter.meta.disabled).to.be(false); - }); - }); - - it('should work without appState', function () { - appState = undefined; - globalState.filters = filters; - - _.each(globalState.filters, function (filter) { - expect(filter.meta.disabled).to.be(false); - }); - - queryFilter.toggleAll(); - - _.each(globalState.filters, function (filter) { - expect(filter.meta.disabled).to.be(true); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/filter_manager/__tests__/filter_manager.js b/src/legacy/ui/public/filter_manager/__tests__/filter_generator.js similarity index 87% rename from src/legacy/ui/public/filter_manager/__tests__/filter_manager.js rename to src/legacy/ui/public/filter_manager/__tests__/filter_generator.js index d33a92c7a0ac3..a7fd90653fd9c 100644 --- a/src/legacy/ui/public/filter_manager/__tests__/filter_manager.js +++ b/src/legacy/ui/public/filter_manager/__tests__/filter_generator.js @@ -22,11 +22,11 @@ import sinon from 'sinon'; import MockState from 'fixtures/mock_state'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { FilterManagerProvider } from '..'; +import { getFilterGenerator } from '..'; import { FilterBarQueryFilterProvider } from '../../filter_manager/query_filter'; import { getPhraseScript } from '@kbn/es-query'; let queryFilter; -let filterManager; +let filterGen; let appState; function checkAddFilters(length, comps, idx) { @@ -56,10 +56,10 @@ describe('Filter Manager', function () { )); beforeEach(ngMock.inject(function (_$rootScope_, Private) { - filterManager = Private(FilterManagerProvider); // mock required queryFilter methods, used in the manager queryFilter = Private(FilterBarQueryFilterProvider); + filterGen = getFilterGenerator(queryFilter); sinon.stub(queryFilter, 'getAppFilters').callsFake(() => appState.filters); sinon.stub(queryFilter, 'addFilters').callsFake((filters) => { if (!Array.isArray(filters)) filters = [filters]; @@ -71,11 +71,11 @@ describe('Filter Manager', function () { })); it('should have an `add` function', function () { - expect(filterManager.add).to.be.a(Function); + expect(filterGen.add).to.be.a(Function); }); it('should add a filter', function () { - filterManager.add('myField', 1, '+', 'myIndex'); + filterGen.add('myField', 1, '+', 'myIndex'); expect(queryFilter.addFilters.callCount).to.be(1); checkAddFilters(1, [{ meta: { index: 'myIndex', negate: false }, @@ -84,7 +84,7 @@ describe('Filter Manager', function () { }); it('should add multiple filters if passed an array of values', function () { - filterManager.add('myField', [1, 2, 3], '+', 'myIndex'); + filterGen.add('myField', [1, 2, 3], '+', 'myIndex'); expect(queryFilter.addFilters.callCount).to.be(1); checkAddFilters(3, [{ meta: { index: 'myIndex', negate: false }, @@ -99,7 +99,7 @@ describe('Filter Manager', function () { }); it('should add an exists filter if _exists_ is used as the field', function () { - filterManager.add('_exists_', 'myField', '+', 'myIndex'); + filterGen.add('_exists_', 'myField', '+', 'myIndex'); checkAddFilters(1, [{ meta: { index: 'myIndex', negate: false }, exists: { field: 'myField' } @@ -107,7 +107,7 @@ describe('Filter Manager', function () { }); it('should negate existing filter instead of added a conflicting filter', function () { - filterManager.add('myField', 1, '+', 'myIndex'); + filterGen.add('myField', 1, '+', 'myIndex'); checkAddFilters(1, [{ meta: { index: 'myIndex', negate: false }, query: { match: { myField: { query: 1, type: 'phrase' } } } @@ -115,30 +115,30 @@ describe('Filter Manager', function () { expect(appState.filters).to.have.length(1); // NOTE: negating exists filters also forces disabled to false - filterManager.add('myField', 1, '-', 'myIndex'); + filterGen.add('myField', 1, '-', 'myIndex'); checkAddFilters(0, null, 1); expect(appState.filters).to.have.length(1); - filterManager.add('_exists_', 'myField', '+', 'myIndex'); + filterGen.add('_exists_', 'myField', '+', 'myIndex'); checkAddFilters(1, [{ meta: { index: 'myIndex', negate: false }, exists: { field: 'myField' } }], 2); expect(appState.filters).to.have.length(2); - filterManager.add('_exists_', 'myField', '-', 'myIndex'); + filterGen.add('_exists_', 'myField', '-', 'myIndex'); checkAddFilters(0, null, 3); expect(appState.filters).to.have.length(2); const scriptedField = { name: 'scriptedField', scripted: true, script: 1, lang: 'painless' }; - filterManager.add(scriptedField, 1, '+', 'myIndex'); + filterGen.add(scriptedField, 1, '+', 'myIndex'); checkAddFilters(1, [{ meta: { index: 'myIndex', negate: false, field: 'scriptedField' }, script: getPhraseScript(scriptedField, 1) }], 4); expect(appState.filters).to.have.length(3); - filterManager.add(scriptedField, 1, '-', 'myIndex'); + filterGen.add(scriptedField, 1, '-', 'myIndex'); checkAddFilters(0, null, 5); expect(appState.filters).to.have.length(3); }); @@ -152,7 +152,7 @@ describe('Filter Manager', function () { expect(appState.filters.length).to.be(1); expect(appState.filters[0].meta.disabled).to.be(true); - filterManager.add('myField', 1, '+', 'myIndex'); + filterGen.add('myField', 1, '+', 'myIndex'); expect(appState.filters.length).to.be(1); expect(appState.filters[0].meta.disabled).to.be(false); }); diff --git a/src/legacy/ui/public/filter_manager/__tests__/query_filter.js b/src/legacy/ui/public/filter_manager/__tests__/query_filter.js deleted file mode 100644 index 8ae0f143ff0d1..0000000000000 --- a/src/legacy/ui/public/filter_manager/__tests__/query_filter.js +++ /dev/null @@ -1,70 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import './_get_filters'; -import './_add_filters'; -import './_remove_filters'; -import './_toggle_filters'; -import './_invert_filters'; -import './_pin_filters'; -import { FilterBarQueryFilterProvider } from '../query_filter'; -let queryFilter; - -describe('Query Filter', function () { - describe('Module', function () { - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (_$rootScope_, Private) { - queryFilter = Private(FilterBarQueryFilterProvider); - })); - - describe('module instance', function () { - it('should use observables', function () { - expect(queryFilter.getUpdates$).to.be.a('function'); - expect(queryFilter.getFetches$).to.be.a('function'); - }); - }); - - describe('module methods', function () { - it('should have methods for getting filters', function () { - expect(queryFilter.getFilters).to.be.a('function'); - expect(queryFilter.getAppFilters).to.be.a('function'); - expect(queryFilter.getGlobalFilters).to.be.a('function'); - }); - - it('should have methods for modifying filters', function () { - expect(queryFilter.addFilters).to.be.a('function'); - expect(queryFilter.toggleFilter).to.be.a('function'); - expect(queryFilter.toggleAll).to.be.a('function'); - expect(queryFilter.removeFilter).to.be.a('function'); - expect(queryFilter.removeAll).to.be.a('function'); - expect(queryFilter.invertFilter).to.be.a('function'); - expect(queryFilter.invertAll).to.be.a('function'); - expect(queryFilter.pinFilter).to.be.a('function'); - expect(queryFilter.pinAll).to.be.a('function'); - }); - - }); - - }); - - describe('Actions', function () { - }); -}); diff --git a/src/legacy/ui/public/filter_manager/filter_manager.js b/src/legacy/ui/public/filter_manager/filter_generator.js similarity index 88% rename from src/legacy/ui/public/filter_manager/filter_manager.js rename to src/legacy/ui/public/filter_manager/filter_generator.js index 0699217d9dcd3..ff034e22aa370 100644 --- a/src/legacy/ui/public/filter_manager/filter_manager.js +++ b/src/legacy/ui/public/filter_manager/filter_generator.js @@ -18,15 +18,13 @@ */ import _ from 'lodash'; -import { FilterBarQueryFilterProvider } from '../filter_manager/query_filter'; import { getPhraseScript } from '@kbn/es-query'; // Adds a filter to a passed state -export function FilterManagerProvider(Private) { - const queryFilter = Private(FilterBarQueryFilterProvider); - const filterManager = {}; +export function getFilterGenerator(queryFilter) { + const filterGen = {}; - filterManager.generate = (field, values, operation, index) => { + filterGen.generate = (field, values, operation, index) => { values = Array.isArray(values) ? values : [values]; const fieldName = _.isObject(field) ? field.name : field; const filters = _.flatten([queryFilter.getAppFilters()]); @@ -90,10 +88,10 @@ export function FilterManagerProvider(Private) { return newFilters; }; - filterManager.add = function (field, values, operation, index) { + filterGen.add = function (field, values, operation, index) { const newFilters = this.generate(field, values, operation, index); return queryFilter.addFilters(newFilters); }; - return filterManager; + return filterGen; } diff --git a/src/legacy/ui/public/filter_manager/index.js b/src/legacy/ui/public/filter_manager/index.js index f713e67941daf..6adc4e0965ccd 100644 --- a/src/legacy/ui/public/filter_manager/index.js +++ b/src/legacy/ui/public/filter_manager/index.js @@ -17,4 +17,4 @@ * under the License. */ -export { FilterManagerProvider } from './filter_manager'; +export { getFilterGenerator } from './filter_generator'; diff --git a/src/legacy/ui/public/filter_manager/query_filter.js b/src/legacy/ui/public/filter_manager/query_filter.js index c1c2993aa0394..19c6e9f5053cc 100644 --- a/src/legacy/ui/public/filter_manager/query_filter.js +++ b/src/legacy/ui/public/filter_manager/query_filter.js @@ -17,400 +17,32 @@ * under the License. */ -import _ from 'lodash'; -import { Subject } from 'rxjs'; +import { FilterStateManager } from 'plugins/data'; -import { onlyDisabled } from './lib/only_disabled'; -import { onlyStateChanged } from './lib/only_state_changed'; -import { uniqFilters } from './lib/uniq_filters'; -import { compareFilters } from './lib/compare_filters'; -import { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; -import { extractTimeFilter } from './lib/extract_time_filter'; -import { changeTimeFilter } from './lib/change_time_filter'; +export function FilterBarQueryFilterProvider(getAppState, globalState) { + // TODO: this is imported here to avoid circular imports. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { data } = require('plugins/data/setup'); + const filterManager = data.filter.filterManager; + const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager); -import { npSetup } from 'ui/new_platform'; - -export function FilterBarQueryFilterProvider(Promise, indexPatterns, $rootScope, getAppState, globalState) { const queryFilter = {}; - const { uiSettings } = npSetup.core; - - const update$ = new Subject(); - const fetch$ = new Subject(); - - queryFilter.getUpdates$ = function () { - return update$.asObservable(); - }; - - queryFilter.getFetches$ = function () { - return fetch$.asObservable(); - }; - - queryFilter.getFilters = function () { - const compareOptions = { disabled: true, negate: true }; - const appFilters = queryFilter.getAppFilters(); - const globalFilters = queryFilter.getGlobalFilters(); - - return uniqFilters(globalFilters.concat(appFilters), compareOptions); - }; - - queryFilter.getAppFilters = function () { - const appState = getAppState(); - if (!appState || !appState.filters) return []; - - // Work around for https://github.com/elastic/kibana/issues/5896 - appState.filters = validateStateFilters(appState); - - return (appState.filters) ? _.map(appState.filters, appendStoreType('appState')) : []; - }; - - queryFilter.getGlobalFilters = function () { - if (!globalState.filters) return []; - - // Work around for https://github.com/elastic/kibana/issues/5896 - globalState.filters = validateStateFilters(globalState); - - return _.map(globalState.filters, appendStoreType('globalState')); - }; - - /** - * Adds new filters to the scope and state - * @param {object|array} filters Filter(s) to add - * @param {bool} global Whether the filter should be added to global state - * @returns {Promise} filter map promise - */ - queryFilter.addFilters = function (filters, addToGlobalState) { - if (addToGlobalState === undefined) { - addToGlobalState = uiSettings.get('filters:pinnedByDefault'); - } - - // Determine the state for the new filter (whether to pass the filter through other apps or not) - const appState = getAppState(); - const filterState = addToGlobalState ? globalState : appState; - - if (!Array.isArray(filters)) { - filters = [filters]; - } - - return Promise.resolve(mapAndFlattenFilters(indexPatterns, filters)) - .then(function (filters) { - if (!filterState.filters) { - filterState.filters = []; - } - - filterState.filters = filterState.filters.concat(filters); - }); + queryFilter.getUpdates$ = filterManager.getUpdates$.bind(filterManager); + queryFilter.getFetches$ = filterManager.getFetches$.bind(filterManager); + queryFilter.getFilters = filterManager.getFilters.bind(filterManager); + queryFilter.getAppFilters = filterManager.getAppFilters.bind(filterManager); + queryFilter.getGlobalFilters = filterManager.getGlobalFilters.bind(filterManager); + queryFilter.removeFilter = filterManager.removeFilter.bind(filterManager); + queryFilter.invertFilter = filterManager.invertFilter.bind(filterManager); + queryFilter.addFilters = filterManager.addFilters.bind(filterManager); + queryFilter.setFilters = filterManager.setFilters.bind(filterManager); + queryFilter.addFiltersAndChangeTimeFilter = filterManager.addFiltersAndChangeTimeFilter.bind(filterManager); + queryFilter.removeAll = filterManager.removeAll.bind(filterManager); + + queryFilter.destroy = () => { + filterManager.destroy(); + filterStateManager.destroy(); }; - /** - * Removes the filter from the proper state - * @param {object} matchFilter The filter to remove - */ - queryFilter.removeFilter = function (matchFilter) { - const appState = getAppState(); - const filter = _.omit(matchFilter, ['$$hashKey']); - let state; - let index; - - // check for filter in appState - if (appState) { - index = _.findIndex(appState.filters, filter); - if (index !== -1) state = appState; - } - - // if not found, check for filter in globalState - if (!state) { - index = _.findIndex(globalState.filters, filter); - if (index !== -1) state = globalState; - else return; // not found in either state, do nothing - } - - state.filters.splice(index, 1); - }; - - /** - * Removes all filters - */ - queryFilter.removeAll = function () { - const appState = getAppState(); - appState.filters = []; - globalState.filters = []; - }; - - /** - * Toggles the filter between enabled/disabled. - * @param {object} filter The filter to toggle - & @param {boolean} force Disabled true/false - * @returns {object} updated filter - */ - queryFilter.toggleFilter = function (filter, force) { - // Toggle the disabled flag - const disabled = _.isUndefined(force) ? !filter.meta.disabled : !!force; - filter.meta.disabled = disabled; - return filter; - }; - - /** - * Disables all filters - * @params {boolean} force Disable/enable all filters - */ - queryFilter.toggleAll = function (force) { - function doToggle(filter) { - queryFilter.toggleFilter(filter, force); - } - - executeOnFilters(doToggle); - }; - - - /** - * Inverts the negate value on the filter - * @param {object} filter The filter to toggle - * @returns {object} updated filter - */ - queryFilter.invertFilter = function (filter) { - // Toggle the negate meta state - filter.meta.negate = !filter.meta.negate; - return filter; - }; - - /** - * Inverts all filters - * @returns {object} Resulting updated filter list - */ - queryFilter.invertAll = function () { - executeOnFilters(queryFilter.invertFilter); - }; - - - /** - * Pins the filter to the global state - * @param {object} filter The filter to pin - * @param {boolean} force pinned state - * @returns {object} updated filter - */ - queryFilter.pinFilter = function (filter, force) { - const appState = getAppState(); - if (!appState) return filter; - - // ensure that both states have a filters property - if (!Array.isArray(globalState.filters)) globalState.filters = []; - if (!Array.isArray(appState.filters)) appState.filters = []; - - const appIndex = _.findIndex(appState.filters, appFilter => _.isEqual(appFilter, filter)); - - if (appIndex !== -1 && force !== false) { - appState.filters.splice(appIndex, 1); - globalState.filters.push(filter); - } else { - const globalIndex = _.findIndex(globalState.filters, globalFilter => _.isEqual(globalFilter, filter)); - - if (globalIndex === -1 || force === true) return filter; - - globalState.filters.splice(globalIndex, 1); - appState.filters.push(filter); - } - - return filter; - }; - - /** - * Pins all filters - * @params {boolean} force Pin/Unpin all filters - */ - queryFilter.pinAll = function (force) { - function pin(filter) { - queryFilter.pinFilter(filter, force); - } - - executeOnFilters(pin); - }; - - queryFilter.setFilters = filters => { - return Promise.resolve(mapAndFlattenFilters(indexPatterns, filters)) - .then(mappedFilters => { - const appState = getAppState(); - const [globalFilters, appFilters] = _.partition(mappedFilters, filter => { - return filter.$state.store === 'globalState'; - }); - globalState.filters = globalFilters; - if (appState) appState.filters = appFilters; - }); - }; - - queryFilter.addFiltersAndChangeTimeFilter = async filters => { - const timeFilter = await extractTimeFilter(indexPatterns, filters); - if (timeFilter) changeTimeFilter(timeFilter); - queryFilter.addFilters(filters.filter(filter => filter !== timeFilter)); - }; - - initWatchers(); - return queryFilter; - - /** - * Rids filter list of null values and replaces state if any nulls are found - */ - function validateStateFilters(state) { - const compacted = _.compact(state.filters); - if (state.filters.length !== compacted.length) { - state.filters = compacted; - state.replace(); - } - return state.filters; - } - - - /** - * Saves both app and global states, ensuring filters are persisted - * @returns {object} Resulting filter list, app and global combined - */ - function saveState() { - const appState = getAppState(); - if (appState) appState.save(); - globalState.save(); - } - - function appendStoreType(type) { - return function (filter) { - filter.$state = { - store: type - }; - return filter; - }; - } - - // helper to run a function on all filters in all states - function executeOnFilters(fn) { - const appState = getAppState(); - let globalFilters = []; - let appFilters = []; - - if (globalState.filters) globalFilters = globalState.filters; - if (appState && appState.filters) appFilters = appState.filters; - - globalFilters.concat(appFilters).forEach(fn); - } - - function mergeStateFilters(gFilters, aFilters, compareOptions) { - // ensure we don't mutate the filters passed in - const globalFilters = gFilters ? _.cloneDeep(gFilters) : []; - const appFilters = aFilters ? _.cloneDeep(aFilters) : []; - - // existing globalFilters should be mutated by appFilters - _.each(appFilters, function (filter, i) { - const match = _.find(globalFilters, function (globalFilter) { - return compareFilters(globalFilter, filter, compareOptions); - }); - - // no match, do nothing - if (!match) return; - - // matching filter in globalState, update global and remove from appState - _.assign(match.meta, filter.meta); - appFilters.splice(i, 1); - }); - - // Reverse the order of globalFilters and appFilters, since uniqFilters - // will throw out duplicates from the back of the array, but we want - // newer filters to overwrite previously created filters. - globalFilters.reverse(); - appFilters.reverse(); - - return [ - // Reverse filters after uniq again, so they are still in the order, they - // were before updating them - uniqFilters(globalFilters).reverse(), - uniqFilters(appFilters).reverse() - ]; - } - - /** - * Initializes state watchers that use the event emitter - * @returns {void} - */ - function initWatchers() { - let removeAppStateWatchers; - - $rootScope.$watch(getAppState, function () { - removeAppStateWatchers && removeAppStateWatchers(); - removeAppStateWatchers = initAppStateWatchers(); - }); - - function initAppStateWatchers() { - // multi watch on the app and global states - const stateWatchers = [{ - fn: $rootScope.$watch, - deep: true, - get: queryFilter.getGlobalFilters - }, { - fn: $rootScope.$watch, - deep: true, - get: queryFilter.getAppFilters - }]; - - // when states change, use event emitter to trigger updates and fetches - return $rootScope.$watchMulti(stateWatchers, function (next, prev) { - // prevent execution on watcher instantiation - if (_.isEqual(next, prev)) return; - - let doUpdate = false; - let doFetch = false; - - // reconcile filter in global and app states - const filters = mergeStateFilters(next[0], next[1]); - const [globalFilters, appFilters] = filters; - const appState = getAppState(); - - // save the state, as it may have updated - const globalChanged = !_.isEqual(next[0], globalFilters); - const appChanged = !_.isEqual(next[1], appFilters); - - // the filters were changed, apply to state (re-triggers this watcher) - if (globalChanged || appChanged) { - globalState.filters = globalFilters; - if (appState) appState.filters = appFilters; - return; - } - - // check for actions, bail if we're done - getActions(); - if (doUpdate) { - // save states and emit the required events - saveState(); - update$.next(); - - if (doFetch) { - fetch$.next(); - } - } - - // iterate over each state type, checking for changes - function getActions() { - let newFilters = []; - let oldFilters = []; - - stateWatchers.forEach(function (watcher, i) { - const nextVal = next[i]; - const prevVal = prev[i]; - newFilters = newFilters.concat(nextVal); - oldFilters = oldFilters.concat(prevVal); - - // no update or fetch if there was no change - if (nextVal === prevVal) return; - - if (nextVal) doUpdate = true; - - // don't trigger fetch when only disabled filters - if (!onlyDisabled(nextVal, prevVal)) doFetch = true; - }); - - // make sure change wasn't only a state move - // checking length first is an optimization - if (doFetch && newFilters.length === oldFilters.length) { - if (onlyStateChanged(newFilters, oldFilters)) doFetch = false; - } - } - }); - } - } } diff --git a/src/legacy/ui/public/filter_manager/push_filters.js b/src/legacy/ui/public/vis/push_filters.js similarity index 100% rename from src/legacy/ui/public/filter_manager/push_filters.js rename to src/legacy/ui/public/vis/push_filters.js diff --git a/src/legacy/ui/public/vis/vis_filters/vis_filters.js b/src/legacy/ui/public/vis/vis_filters/vis_filters.js index f013233c345b2..ce05ca2bea145 100644 --- a/src/legacy/ui/public/vis/vis_filters/vis_filters.js +++ b/src/legacy/ui/public/vis/vis_filters/vis_filters.js @@ -18,11 +18,10 @@ */ import _ from 'lodash'; -import { pushFilterBarFilters } from '../../filter_manager/push_filters'; +import { pushFilterBarFilters } from '../push_filters'; import { onBrushEvent } from './brush_event'; -import { uniqFilters } from '../../filter_manager/lib/uniq_filters'; +import { uniqFilters } from '../../../../core_plugins/data/public'; import { toggleFilterNegated } from '@kbn/es-query'; - /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter * terms based on a specific cell in the tabified data. diff --git a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts index e5a4eb122dc88..24a806bf27b18 100644 --- a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts +++ b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts @@ -26,6 +26,26 @@ import { VisResponseData } from './types'; import { Inspector } from '../../inspector'; import { EmbeddedVisualizeHandler } from './embedded_visualize_handler'; +jest.mock('ui/new_platform', () => ({ + npStart: { + core: { + i18n: { + Context: {}, + }, + chrome: { + recentlyAccessed: false, + }, + }, + }, + npSetup: { + core: { + uiSettings: { + get: () => true, + }, + }, + }, +})); + describe('EmbeddedVisualizeHandler', () => { let handler: any; let div: HTMLElement; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap b/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap index 0eeb18db70850..c2f601de0ba38 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap @@ -2,11 +2,11 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles input_control_vis function 1`] = `"input_control_vis visConfig='{\\"some\\":\\"nested\\",\\"data\\":{\\"here\\":true}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles markdown function 1`] = `"markdownvis '## hello _markdown_' fontSize=12 openLinksInNewTab=true "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles markdown function 1`] = `"markdownvis '## hello _markdown_' font={font size=12} openLinksInNewTab=true "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metric function with buckets 1`] = `"kibana_metric visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metrics\\":[0,1],\\"bucket\\":2}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metric function with buckets 1`] = `"metricvis metric={visdimension 0 } metric={visdimension 1 } "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metric function without buckets 1`] = `"kibana_metric visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metrics\\":[0,1]}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metric function without buckets 1`] = `"metricvis metric={visdimension 0 } metric={visdimension 1 } "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metrics/tsvb function 1`] = `"tsvb params='{\\"foo\\":\\"bar\\"}' uiState='{}' "`; @@ -28,7 +28,7 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function with boolean param showLabel 1`] = `"tagcloud metric={visdimension 0} showLabel=false "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function with buckets 1`] = `"tagcloud metric={visdimension 0} bucket={visdimension 1 } "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function with buckets 1`] = `"tagcloud metric={visdimension 0} bucket={visdimension 1 } "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function without buckets 1`] = `"tagcloud metric={visdimension 0} "`; @@ -36,6 +36,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles timelion function 1`] = `"timelion_vis expression='foo' interval='bar' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles undefined markdown function 1`] = `"markdownvis '' fontSize=12 openLinksInNewTab=true "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles undefined markdown function 1`] = `"markdownvis '' font={font size=12} openLinksInNewTab=true "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles vega function 1`] = `"vega spec='this is a test' "`; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js index 435108c8023e7..51a66b33c9faa 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js @@ -157,15 +157,15 @@ describe('visualize loader pipeline helpers: build pipeline', () => { describe('handles metric function', () => { const params = { metric: {} }; it('without buckets', () => { - const schemas = { metric: [0, 1] }; + const schemas = { metric: [{ accessor: 0 }, { accessor: 1 }] }; const actual = buildPipelineVisFunction.metric({ params }, schemas); expect(actual).toMatchSnapshot(); }); it('with buckets', () => { const schemas = { - metric: [0, 1], - group: [2] + metric: [{ accessor: 0 }, { accessor: 1 }], + group: [{ accessor: 2 }] }; const actual = buildPipelineVisFunction.metric({ params }, schemas); expect(actual).toMatchSnapshot(); diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts index 1488fa0658912..dee76e8bb9658 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts @@ -193,17 +193,55 @@ export const getSchemas = (vis: Vis, timeRange?: any): Schemas => { }; export const prepareJson = (variable: string, data: object): string => { + if (data === undefined) { + return ''; + } return `${variable}='${JSON.stringify(data) .replace(/\\/g, `\\\\`) .replace(/'/g, `\\'`)}' `; }; -export const prepareString = (variable: string, data: string): string => { +export const escapeString = (data: string): string => { + return data.replace(/\\/g, `\\\\`).replace(/'/g, `\\'`); +}; + +export const prepareString = (variable: string, data?: string): string => { + if (data === undefined) { + return ''; + } return `${variable}='${escapeString(data)}' `; }; -export const escapeString = (data: string): string => { - return data.replace(/\\/g, `\\\\`).replace(/'/g, `\\'`); +export const prepareValue = (variable: string, data: any, raw: boolean = false) => { + if (data === undefined) { + return ''; + } + if (raw) { + return `${variable}=${data} `; + } + switch (typeof data) { + case 'string': + return prepareString(variable, data); + case 'object': + return prepareJson(variable, data); + default: + return `${variable}=${data} `; + } +}; + +export const prepareDimension = (variable: string, data: any) => { + if (data === undefined) { + return ''; + } + + let expr = `${variable}={visdimension ${data.accessor} `; + if (data.format) { + expr += prepareValue('format', data.format.id); + expr += prepareJson('formatParams', data.format.params); + } + expr += '} '; + + return expr; }; export const buildPipelineVisFunction: BuildPipelineVisFunction = { @@ -231,12 +269,8 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { escapedMarkdown = escapeString(markdown.toString()); } let expr = `markdownvis '${escapedMarkdown}' `; - if (fontSize) { - expr += ` fontSize=${fontSize} `; - } - if (openLinksInNewTab) { - expr += `openLinksInNewTab=${openLinksInNewTab} `; - } + expr += prepareValue('font', `{font size=${fontSize}}`, true); + expr += prepareValue('openLinksInNewTab', openLinksInNewTab); return expr; }, table: (visState, schemas) => { @@ -247,41 +281,55 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { return `kibana_table ${prepareJson('visConfig', visConfig)}`; }, metric: (visState, schemas) => { - const visConfig = { - ...visState.params, - ...buildVisConfig.metric(schemas), - }; - return `kibana_metric ${prepareJson('visConfig', visConfig)}`; + const { + percentageMode, + useRanges, + colorSchema, + metricColorMode, + colorsRange, + labels, + invertColors, + style, + } = visState.params.metric; + const { metrics, bucket } = buildVisConfig.metric(schemas).dimensions; + + let expr = `metricvis `; + expr += prepareValue('percentage', percentageMode); + expr += prepareValue('colorScheme', colorSchema); + expr += prepareValue('colorMode', metricColorMode); + expr += prepareValue('useRanges', useRanges); + expr += prepareValue('invertColors', invertColors); + expr += prepareValue('showLabels', labels && labels.show); + if (style) { + expr += prepareValue('bgFill', style.bgFill); + expr += prepareValue('font', `{font size=${style.fontSize}}`, true); + expr += prepareValue('subText', style.subText); + expr += prepareDimension('bucket', bucket); + } + + if (colorsRange) { + colorsRange.forEach((range: any) => { + expr += prepareValue('colorRange', `{range from=${range.from} to=${range.to}}`, true); + }); + } + + metrics.forEach((metric: SchemaConfig) => { + expr += prepareDimension('metric', metric); + }); + + return expr; }, tagcloud: (visState, schemas) => { const { scale, orientation, minFontSize, maxFontSize, showLabel } = visState.params; const { metric, bucket } = buildVisConfig.tagcloud(schemas); let expr = `tagcloud metric={visdimension ${metric.accessor}} `; + expr += prepareValue('scale', scale); + expr += prepareValue('orientation', orientation); + expr += prepareValue('minFontSize', minFontSize); + expr += prepareValue('maxFontSize', maxFontSize); + expr += prepareValue('showLabel', showLabel); + expr += prepareDimension('bucket', bucket); - if (scale) { - expr += `scale='${scale}' `; - } - if (orientation) { - expr += `orientation='${orientation}' `; - } - if (minFontSize) { - expr += `minFontSize=${minFontSize} `; - } - if (maxFontSize) { - expr += `maxFontSize=${maxFontSize} `; - } - if (showLabel !== undefined) { - expr += `showLabel=${showLabel} `; - } - - if (bucket) { - expr += ` bucket={visdimension ${bucket.accessor} `; - if (bucket.format) { - expr += `format=${bucket.format.id} `; - expr += prepareJson('formatParams', bucket.format.params); - } - expr += '} '; - } return expr; }, region_map: (visState, schemas) => { diff --git a/src/legacy/ui/public/visualize/loader/types.ts b/src/legacy/ui/public/visualize/loader/types.ts index 33aea5151762e..4f34f6219621f 100644 --- a/src/legacy/ui/public/visualize/loader/types.ts +++ b/src/legacy/ui/public/visualize/loader/types.ts @@ -17,8 +17,8 @@ * under the License. */ -import { TimeRange } from 'ui/timefilter/time_history'; import { Filter } from '@kbn/es-query'; +import { TimeRange } from 'ui/timefilter/time_history'; import { Query } from 'src/legacy/core_plugins/data/public'; import { SearchSource } from '../../courier'; import { PersistedState } from '../../persisted_state'; diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index 0df9e03938262..50c46334885a6 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -572,7 +572,20 @@ export class WebElementWrapper { const screenshot = await this.driver.takeScreenshot(); const buffer = Buffer.from(screenshot.toString(), 'base64'); const { width, height, x, y } = await this.getPosition(); + const windowWidth = await this.driver.executeScript('return window.document.body.clientWidth'); const src = PNG.sync.read(buffer); + if (src.width > windowWidth) { + // on linux size of screenshot is double size of screen, scale it down + src.width = src.width / 2; + src.height = src.height / 2; + let h = false; + let v = false; + src.data = src.data.filter((d: any, i: number) => { + h = i % 4 ? h : !h; + v = i % (src.width * 2 * 4) ? v : !v; + return h && v; + }); + } const dst = new PNG({ width, height }); PNG.bitblt(src, dst, x, y, width, height, 0, 0); return PNG.sync.write(dst); diff --git a/test/interpreter_functional/screenshots/baseline/combined_test.png b/test/interpreter_functional/screenshots/baseline/combined_test.png index 8e2a52bf44c1b..376d2b54bd08d 100644 Binary files a/test/interpreter_functional/screenshots/baseline/combined_test.png and b/test/interpreter_functional/screenshots/baseline/combined_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png index 8e2a52bf44c1b..073952ac6c969 100644 Binary files a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png and b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_all_data.png b/test/interpreter_functional/screenshots/baseline/metric_all_data.png new file mode 100644 index 0000000000000..f62967f95a2fa Binary files /dev/null and b/test/interpreter_functional/screenshots/baseline/metric_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png new file mode 100644 index 0000000000000..1db494849a23d Binary files /dev/null and b/test/interpreter_functional/screenshots/baseline/metric_multi_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_percentage.png b/test/interpreter_functional/screenshots/baseline/metric_percentage.png new file mode 100644 index 0000000000000..8e2a52bf44c1b Binary files /dev/null and b/test/interpreter_functional/screenshots/baseline/metric_percentage.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png new file mode 100644 index 0000000000000..b5bd2e1dd8839 Binary files /dev/null and b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png index 8e2a52bf44c1b..36c7d6c5e1013 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png index 8e2a52bf44c1b..7b8288a8bf908 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png index 8e2a52bf44c1b..7cca731c267ba 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png index 8e2a52bf44c1b..af5eaba74b219 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png differ diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 4224c95f2f089..2a2d8dff98b8f 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0},"metrics":[{"accessor":1,"format":{"id":"number"},"params":{}}]}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 4224c95f2f089..2a2d8dff98b8f 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0},"metrics":[{"accessor":1,"format":{"id":"number"},"params":{}}]}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json new file mode 100644 index 0000000000000..ebed9f8e47435 --- /dev/null +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json new file mode 100644 index 0000000000000..ac4860c6735bf --- /dev/null +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage.json b/test/interpreter_functional/snapshots/baseline/metric_percentage.json new file mode 100644 index 0000000000000..e171a65be8bab --- /dev/null +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json new file mode 100644 index 0000000000000..ae27e624df5bb --- /dev/null +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -0,0 +1 @@ +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 4224c95f2f089..2a2d8dff98b8f 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0},"metrics":[{"accessor":1,"format":{"id":"number"},"params":{}}]}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/test_suites/run_pipeline/basic.js b/test/interpreter_functional/test_suites/run_pipeline/basic.js index 9441531b065f1..aa35750dbfe70 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/basic.js +++ b/test/interpreter_functional/test_suites/run_pipeline/basic.js @@ -51,8 +51,7 @@ export default function ({ getService, updateBaselines }) { {"id":"2","enabled":true,"type":"terms","schema":"segment","params": {"field":"response.raw","size":4,"order":"desc","orderBy":"1"} }]' | - kibana_metric - visConfig='{"dimensions":{"metrics":[{"accessor":1,"format":{"id":"number"},"params":{}}],"bucket":{"accessor":0}}}' + metricVis metric={visdimension 1 format="number"} bucket={visdimension 0} `; // we can execute an expression and validate the result manually: @@ -95,12 +94,11 @@ export default function ({ getService, updateBaselines }) { // we reuse that response to render 3 different charts and compare screenshots with baselines const tagCloudExpr = - `tagcloud visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`; + `tagcloud metric={visdimension 1 format="number"} bucket={visdimension 0}'`; await expectExpression('partial_test_1', tagCloudExpr, context).toMatchScreenshot(); const metricExpr = - `kibana_metric - visConfig='{"dimensions":{"metrics":[{"accessor":1,"format":{"id":"number"}}],"bucket":{"accessor":0}}}'`; + `metricVis metric={visdimension 1 format="number"} bucket={visdimension 0}'`; await expectExpression('partial_test_2', metricExpr, context).toMatchScreenshot(); const regionMapExpr = diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.js b/test/interpreter_functional/test_suites/run_pipeline/index.js index 3e3fd953fe9c2..3c1ce2314f55f 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.js +++ b/test/interpreter_functional/test_suites/run_pipeline/index.js @@ -40,5 +40,6 @@ export default function ({ getService, getPageObjects, loadTestFile }) { loadTestFile(require.resolve('./basic')); loadTestFile(require.resolve('./tag_cloud')); + loadTestFile(require.resolve('./metric')); }); } diff --git a/test/interpreter_functional/test_suites/run_pipeline/metric.js b/test/interpreter_functional/test_suites/run_pipeline/metric.js new file mode 100644 index 0000000000000..4dc652dea7487 --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/metric.js @@ -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 { expectExpressionProvider } from './helpers'; + +// this file showcases how to use testing utilities defined in helpers.js together with the kbn_tp_run_pipeline +// test plugin to write autmated tests for interprete +export default function ({ getService, updateBaselines }) { + + let expectExpression; + describe('metricVis pipeline expression tests', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + // we should not use this for tests like the ones below. this should be unit tested. + // - tests against a single function could easily be written as unit tests (and should be) + describe('correctly renders metric', () => { + let dataContext; + before(async () => { + const expression = `kibana | kibana_context | esaggs index='logstash-*' aggConfigs='[ + {"id":"1","enabled":true,"type":"count","schema":"metric","params":{}}, + {"id":"1","enabled":true,"type":"max","schema":"metric","params": + {"field":"bytes"} + }, + {"id":"2","enabled":true,"type":"terms","schema":"segment","params": + {"field":"response.raw","size":4,"order":"desc","orderBy":"1"} + }]'`; + // we execute the part of expression that fetches the data and store its response + dataContext = await expectExpression('partial_metric_test', expression).getResponse(); + }); + + it.skip('with invalid data', async () => { + const expression = 'metricVis metric={visdimension 0}'; + await (await expectExpression('metric_invalid_data', expression).toMatchSnapshot()).toMatchScreenshot(); + }); + + it('with single metric data', async () => { + const expression = 'metricVis metric={visdimension 0}'; + await (await expectExpression('metric_single_metric_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + }); + + it('with multiple metric data', async () => { + const expression = 'metricVis metric={visdimension 0} metric={visdimension 1}'; + await expectExpression('metric_multi_metric_data', expression, dataContext).toMatchSnapshot(); + }); + + it('with metric and bucket data', async () => { + const expression = 'metricVis metric={visdimension 0} bucket={visdimension 2}'; + await (await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot()).toMatchScreenshot(); + }); + + it('with percentage option', async () => { + const expression = 'metricVis metric={visdimension 0} percentage=true colorRange={range from=0 to=1000}'; + await expectExpression('metric_percentage', expression, dataContext).toMatchSnapshot(); + }); + }); + }); +}