From d8f44220c00ed51ab86ee5f201ff70f2235b1214 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Thu, 8 Feb 2024 15:37:50 +0100 Subject: [PATCH] [Discover] Include global filters when opening a saved search (#175814) - Closes https://github.com/elastic/kibana/issues/171212 ## Summary Discover ignored global filters when loading a saved search, because it loads a saved search before it starts syncing with global URL state. This PR does not change the order of events but it adds a manual sync for global filters so they are included in search request anyway. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../discover_saved_search_container.ts | 31 +++++- .../main/services/discover_state.ts | 1 + .../main/services/load_saved_search.ts | 25 ++++- .../apps/discover/group1/_url_state.ts | 102 ++++++++++++++++++ 4 files changed, 155 insertions(+), 4 deletions(-) diff --git a/src/plugins/discover/public/application/main/services/discover_saved_search_container.ts b/src/plugins/discover/public/application/main/services/discover_saved_search_container.ts index ef88aba74d7db..ecae2208bffcc 100644 --- a/src/plugins/discover/public/application/main/services/discover_saved_search_container.ts +++ b/src/plugins/discover/public/application/main/services/discover_saved_search_container.ts @@ -8,6 +8,7 @@ import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { BehaviorSubject } from 'rxjs'; +import { cloneDeep } from 'lodash'; import { COMPARE_ALL_OPTIONS, FilterCompareOptions } from '@kbn/es-query'; import type { SearchSourceFields } from '@kbn/data-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/common'; @@ -110,6 +111,11 @@ export interface DiscoverSavedSearchContainer { * @param params */ update: (params: UpdateParams) => SavedSearch; + /** + * Passes filter manager filters to saved search filters + * @param params + */ + updateWithFilterManagerFilters: () => SavedSearch; } export function getSavedSearchContainer({ @@ -169,6 +175,26 @@ export function getSavedSearchContainer({ } return { id }; }; + + const assignNextSavedSearch = ({ nextSavedSearch }: { nextSavedSearch: SavedSearch }) => { + const hasChanged = !isEqualSavedSearch(savedSearchInitial$.getValue(), nextSavedSearch); + hasChanged$.next(hasChanged); + savedSearchCurrent$.next(nextSavedSearch); + }; + + const updateWithFilterManagerFilters = () => { + const nextSavedSearch: SavedSearch = { + ...getState(), + }; + + nextSavedSearch.searchSource.setField('filter', cloneDeep(services.filterManager.getFilters())); + + assignNextSavedSearch({ nextSavedSearch }); + + addLog('[savedSearch] updateWithFilterManagerFilters done', nextSavedSearch); + return nextSavedSearch; + }; + const update = ({ nextDataView, nextState, useFilterAndQueryServices }: UpdateParams) => { addLog('[savedSearch] update', { nextDataView, nextState }); @@ -186,9 +212,7 @@ export function getSavedSearchContainer({ useFilterAndQueryServices, }); - const hasChanged = !isEqualSavedSearch(savedSearchInitial$.getValue(), nextSavedSearch); - hasChanged$.next(hasChanged); - savedSearchCurrent$.next(nextSavedSearch); + assignNextSavedSearch({ nextSavedSearch }); addLog('[savedSearch] update done', nextSavedSearch); return nextSavedSearch; @@ -221,6 +245,7 @@ export function getSavedSearchContainer({ persist, set, update, + updateWithFilterManagerFilters, }; } diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index af3675156a93d..79577a8f8e616 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -357,6 +357,7 @@ export function getDiscoverStateContainer({ dataStateContainer, internalStateContainer, savedSearchContainer, + globalStateContainer, services, setDataView, }); diff --git a/src/plugins/discover/public/application/main/services/load_saved_search.ts b/src/plugins/discover/public/application/main/services/load_saved_search.ts index a921e7a69e58c..30ed1792a50e0 100644 --- a/src/plugins/discover/public/application/main/services/load_saved_search.ts +++ b/src/plugins/discover/public/application/main/services/load_saved_search.ts @@ -22,6 +22,7 @@ import { DiscoverAppStateContainer, getInitialState, } from './discover_app_state_container'; +import { DiscoverGlobalStateContainer } from './discover_global_state_container'; import { DiscoverServices } from '../../../build_services'; interface LoadSavedSearchDeps { @@ -29,6 +30,7 @@ interface LoadSavedSearchDeps { dataStateContainer: DiscoverDataStateContainer; internalStateContainer: DiscoverInternalStateContainer; savedSearchContainer: DiscoverSavedSearchContainer; + globalStateContainer: DiscoverGlobalStateContainer; services: DiscoverServices; setDataView: DiscoverStateContainer['actions']['setDataView']; } @@ -44,7 +46,13 @@ export const loadSavedSearch = async ( ): Promise => { addLog('[discoverState] loadSavedSearch'); const { savedSearchId } = params ?? {}; - const { appStateContainer, internalStateContainer, savedSearchContainer, services } = deps; + const { + appStateContainer, + internalStateContainer, + savedSearchContainer, + globalStateContainer, + services, + } = deps; const appStateExists = !appStateContainer.isEmptyURL(); const appState = appStateExists ? appStateContainer.getState() : undefined; @@ -59,6 +67,15 @@ export const loadSavedSearch = async ( services.filterManager.setAppFilters([]); services.data.query.queryString.clearQuery(); + // Sync global filters (coming from URL) to filter manager. + // It needs to be done manually here as `syncGlobalQueryStateWithUrl` is being called after this `loadSavedSearch` function. + const globalFilters = globalStateContainer?.get()?.filters; + const shouldUpdateWithGlobalFilters = + globalFilters?.length && !services.filterManager.getGlobalFilters()?.length; + if (shouldUpdateWithGlobalFilters) { + services.filterManager.setGlobalFilters(globalFilters); + } + // reset appState in case a saved search with id is loaded and // the url is empty so the saved search is loaded in a clean // state else it might be updated by the previous app state @@ -103,6 +120,10 @@ export const loadSavedSearch = async ( // Update all other services and state containers by the next saved search updateBySavedSearch(nextSavedSearch, deps); + if (!appState && shouldUpdateWithGlobalFilters) { + nextSavedSearch = savedSearchContainer.updateWithFilterManagerFilters(); + } + return nextSavedSearch; }; @@ -125,6 +146,7 @@ function updateBySavedSearch(savedSearch: SavedSearch, deps: LoadSavedSearchDeps // set data service filters const filters = savedSearch.searchSource.getField('filter'); if (Array.isArray(filters) && filters.length) { + // Saved search SO persists all filters as app filters services.data.query.filterManager.setAppFilters(cloneDeep(filters)); } // some filters may not be valid for this context, so update @@ -134,6 +156,7 @@ function updateBySavedSearch(savedSearch: SavedSearch, deps: LoadSavedSearchDeps if (!isEqual(currentFilters, validFilters)) { services.filterManager.setFilters(validFilters); } + // set data service query const query = savedSearch.searchSource.getField('query'); if (query) { diff --git a/test/functional/apps/discover/group1/_url_state.ts b/test/functional/apps/discover/group1/_url_state.ts index 027e767e8fe3d..e97ac332e8b6e 100644 --- a/test/functional/apps/discover/group1/_url_state.ts +++ b/test/functional/apps/discover/group1/_url_state.ts @@ -11,6 +11,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { + const deployment = getService('deployment'); const browser = getService('browser'); const log = getService('log'); const retry = getService('retry'); @@ -19,6 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); + const dataGrid = getService('dataGrid'); const PageObjects = getPageObjects([ 'common', 'discover', @@ -30,6 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const defaultSettings = { defaultIndex: 'logstash-*', + hideAnnouncements: true, }; describe('discover URL state', () => { @@ -117,5 +120,104 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); expect(await PageObjects.discover.getHitCount()).to.be('11,268'); }); + + it('should merge custom global filters with saved search filters', async () => { + await kibanaServer.uiSettings.update({ + 'timepicker:timeDefaults': + '{ "from": "Sep 18, 2015 @ 19:37:13.000", "to": "Sep 23, 2015 @ 02:30:09.000"}', + }); + await PageObjects.common.navigateToApp('discover'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + await filterBar.addFilter({ + field: 'bytes', + operation: 'is between', + value: { from: '1000', to: '2000' }, + }); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('extension'); + await PageObjects.unifiedFieldList.clickFieldListItemAdd('bytes'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + const totalHitsForOneFilter = '737'; + const totalHitsForTwoFilters = '137'; + + expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForOneFilter); + + await PageObjects.discover.saveSearch('testFilters'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForOneFilter); + + await browser.refresh(); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForOneFilter); + + const url = await browser.getCurrentUrl(); + const savedSearchIdMatch = url.match(/view\/([^?]+)\?/); + const savedSearchId = savedSearchIdMatch?.length === 2 ? savedSearchIdMatch[1] : null; + + expect(typeof savedSearchId).to.be('string'); + + await browser.openNewTab(); + await browser.get(`${deployment.getHostPort()}/app/discover#/view/${savedSearchId}`); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + expect(await dataGrid.getRowsText()).to.eql([ + 'Sep 22, 2015 @ 20:44:05.521jpg1,808', + 'Sep 22, 2015 @ 20:41:53.463png1,969', + 'Sep 22, 2015 @ 20:40:22.952jpg1,576', + 'Sep 22, 2015 @ 20:11:39.532png1,708', + 'Sep 22, 2015 @ 19:45:13.813php1,406', + ]); + + expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForOneFilter); + + await browser.openNewTab(); + await browser.get( + `${deployment.getHostPort()}/app/discover#/view/${savedSearchId}` + + "?_g=(filters:!(('$state':(store:globalState)," + + "meta:(alias:!n,disabled:!f,field:extension.raw,index:'logstash-*'," + + 'key:extension.raw,negate:!f,params:!(png,css),type:phrases,value:!(png,css)),' + + 'query:(bool:(minimum_should_match:1,should:!((match_phrase:(extension.raw:png)),' + + "(match_phrase:(extension.raw:css))))))),query:(language:kuery,query:'')," + + "refreshInterval:(pause:!t,value:60000),time:(from:'2015-09-19T06:31:44.000Z'," + + "to:'2015-09-23T18:31:44.000Z'))" + ); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + const filteredRows = [ + 'Sep 22, 2015 @ 20:41:53.463png1,969', + 'Sep 22, 2015 @ 20:11:39.532png1,708', + 'Sep 22, 2015 @ 18:50:22.335css1,841', + 'Sep 22, 2015 @ 18:40:32.329css1,945', + 'Sep 22, 2015 @ 18:13:35.361css1,752', + ]; + + expect(await dataGrid.getRowsText()).to.eql(filteredRows); + expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForTwoFilters); + await testSubjects.existOrFail('unsavedChangesBadge'); + + await browser.refresh(); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.discover.waitUntilSearchingHasFinished(); + + expect(await dataGrid.getRowsText()).to.eql(filteredRows); + expect(await PageObjects.discover.getHitCount()).to.be(totalHitsForTwoFilters); + await testSubjects.existOrFail('unsavedChangesBadge'); + }); }); }