Skip to content

Commit

Permalink
[Discover] Include global filters when opening a saved search (elasti…
Browse files Browse the repository at this point in the history
…c#175814)

- Closes elastic#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
  • Loading branch information
jughosta authored and CoenWarmer committed Feb 15, 2024
1 parent 8f497da commit ff0db8e
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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 });

Expand All @@ -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;
Expand Down Expand Up @@ -221,6 +245,7 @@ export function getSavedSearchContainer({
persist,
set,
update,
updateWithFilterManagerFilters,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ export function getDiscoverStateContainer({
dataStateContainer,
internalStateContainer,
savedSearchContainer,
globalStateContainer,
services,
setDataView,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import {
DiscoverAppStateContainer,
getInitialState,
} from './discover_app_state_container';
import { DiscoverGlobalStateContainer } from './discover_global_state_container';
import { DiscoverServices } from '../../../build_services';

interface LoadSavedSearchDeps {
appStateContainer: DiscoverAppStateContainer;
dataStateContainer: DiscoverDataStateContainer;
internalStateContainer: DiscoverInternalStateContainer;
savedSearchContainer: DiscoverSavedSearchContainer;
globalStateContainer: DiscoverGlobalStateContainer;
services: DiscoverServices;
setDataView: DiscoverStateContainer['actions']['setDataView'];
}
Expand All @@ -44,7 +46,13 @@ export const loadSavedSearch = async (
): Promise<SavedSearch> => {
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;

Expand All @@ -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
Expand Down Expand Up @@ -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;
};

Expand All @@ -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
Expand All @@ -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) {
Expand Down
102 changes: 102 additions & 0 deletions test/functional/apps/discover/group1/_url_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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',
Expand All @@ -30,6 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {

const defaultSettings = {
defaultIndex: 'logstash-*',
hideAnnouncements: true,
};

describe('discover URL state', () => {
Expand Down Expand Up @@ -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');
});
});
}

0 comments on commit ff0db8e

Please sign in to comment.