diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index d1c7d2d9eba3e..babd1fac274eb 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -143,6 +143,7 @@ export class DashboardAppController { }); // sync initial app filters from state to filterManager + // if there is an existing similar global filter, then leave it as global filterManager.setAppFilters(_.cloneDeep(dashboardStateManager.appState.filters)); // setup syncing of app filters between appState and filterManager const stopSyncingAppFilters = connectToQueryState( @@ -150,7 +151,11 @@ export class DashboardAppController { { set: ({ filters }) => dashboardStateManager.setFilters(filters || []), get: () => ({ filters: dashboardStateManager.appState.filters }), - state$: dashboardStateManager.appState$.pipe(map(state => ({ filters: state.filters }))), + state$: dashboardStateManager.appState$.pipe( + map(state => ({ + filters: state.filters, + })) + ), }, { filters: esFilters.FilterStateStore.APP_STATE, @@ -170,13 +175,16 @@ export class DashboardAppController { } // starts syncing `_g` portion of url with query services - // note: dashboard_state_manager.ts syncs `_a` portion of url // it is important to start this syncing after `dashboardStateManager.syncTimefilterWithDashboard(timefilter);` above is run, - // otherwise it will case redundant browser history record + // otherwise it will case redundant browser history records const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( queryService, kbnUrlStateStorage ); + + // starts syncing `_a` portion of url + dashboardStateManager.startStateSyncing(); + $scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean; const getShouldShowEditHelp = () => diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index 6025f535ae761..13ba3c6d0b60d 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -164,9 +164,6 @@ export class DashboardStateManager { this.changeListeners.forEach(listener => listener({ dirty: this.isDirty })); }); - // make sure url ('_a') matches initial state - this.kbnUrlStateStorage.set(this.STATE_STORAGE_KEY, initialState, { replace: true }); - // setup state syncing utils. state container will be synced with url into `this.STATE_STORAGE_KEY` query param this.stateSyncRef = syncState({ storageKey: this.STATE_STORAGE_KEY, @@ -201,8 +198,10 @@ export class DashboardStateManager { }, stateStorage: this.kbnUrlStateStorage, }); + } - // actually start syncing state with container + public startStateSyncing() { + this.saveState({ replace: true }); this.stateSyncRef.start(); } diff --git a/src/plugins/dashboard/public/application/lib/filter_utils.ts b/src/plugins/dashboard/public/application/lib/filter_utils.ts index 1ec231db0c3d2..775bf90643a21 100644 --- a/src/plugins/dashboard/public/application/lib/filter_utils.ts +++ b/src/plugins/dashboard/public/application/lib/filter_utils.ts @@ -19,7 +19,7 @@ import _ from 'lodash'; import moment, { Moment } from 'moment'; -import { Filter } from 'src/plugins/data/public'; +import { Filter } from '../../../../data/public'; /** * @typedef {Object} QueryFilter diff --git a/src/plugins/dashboard/public/application/lib/update_saved_dashboard.ts b/src/plugins/dashboard/public/application/lib/update_saved_dashboard.ts index 53dc7d9b460de..fc519327b41ee 100644 --- a/src/plugins/dashboard/public/application/lib/update_saved_dashboard.ts +++ b/src/plugins/dashboard/public/application/lib/update_saved_dashboard.ts @@ -22,6 +22,7 @@ import { RefreshInterval, TimefilterContract } from 'src/plugins/data/public'; import { FilterUtils } from './filter_utils'; import { SavedObjectDashboard } from '../../saved_dashboards'; import { DashboardAppState } from '../../types'; +import { esFilters } from '../../../../data/public'; export function updateSavedDashboard( savedDashboard: SavedObjectDashboard, @@ -48,4 +49,10 @@ export function updateSavedDashboard( 'value', ]); savedDashboard.refreshInterval = savedDashboard.timeRestore ? timeRestoreObj : undefined; + + // save only unpinned filters + const unpinnedFilters = savedDashboard + .getFilters() + .filter(filter => !esFilters.isFilterPinned(filter)); + savedDashboard.searchSource.setField('filter', unpinnedFilters); } diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.test.ts b/src/plugins/data/public/query/filter_manager/filter_manager.test.ts index ce574ace45705..3b4ca08cbbf14 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.test.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.test.ts @@ -204,18 +204,19 @@ describe('filter_manager', () => { ).toBe(3); }); - test('should set app filters and remove any duplicated global filters', async function() { - filterManager.addFilters(readyFilters, true); + test('should set app filters and merge them with duplicate global filters', async function() { + const [filter, ...otherFilters] = readyFilters; + filterManager.addFilters(otherFilters, true); const appFilter1 = _.cloneDeep(readyFilters[1]); const appFilter2 = _.cloneDeep(readyFilters[2]); - filterManager.setAppFilters([appFilter1, appFilter2]); + filterManager.setAppFilters([filter, appFilter1, appFilter2]); const newGlobalFilters = filterManager.getGlobalFilters(); const newAppFilters = filterManager.getAppFilters(); - expect(newGlobalFilters).toHaveLength(1); - expect(newAppFilters).toHaveLength(2); + expect(newGlobalFilters).toHaveLength(2); + expect(newAppFilters).toHaveLength(1); }); test('should set global filters and remove any duplicated app filters', async function() { diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index fba1866ebd615..e206286bce147 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -177,13 +177,9 @@ export class FilterManager { public setGlobalFilters(newGlobalFilters: Filter[]) { newGlobalFilters = mapAndFlattenFilters(newGlobalFilters); FilterManager.setFiltersStore(newGlobalFilters, FilterStateStore.GLOBAL_STATE, true); - const { appFilters: currentAppFilters } = this.getPartitionedFilters(); - // remove duplicates from current app filters, to make sure global will take precedence - const filteredAppFilters = currentAppFilters.filter( - appFilter => !newGlobalFilters.find(globalFilter => compareFilters(globalFilter, appFilter)) - ); + const { appFilters } = this.getPartitionedFilters(); const newFilters = this.mergeIncomingFilters({ - appFilters: filteredAppFilters, + appFilters, globalFilters: newGlobalFilters, }); @@ -198,14 +194,9 @@ export class FilterManager { public setAppFilters(newAppFilters: Filter[]) { newAppFilters = mapAndFlattenFilters(newAppFilters); FilterManager.setFiltersStore(newAppFilters, FilterStateStore.APP_STATE, true); - const { globalFilters: currentGlobalFilters } = this.getPartitionedFilters(); - // remove duplicates from current global filters, to make sure app will take precedence - const filteredGlobalFilters = currentGlobalFilters.filter( - globalFilter => !newAppFilters.find(appFilter => compareFilters(appFilter, globalFilter)) - ); - + const { globalFilters } = this.getPartitionedFilters(); const newFilters = this.mergeIncomingFilters({ - globalFilters: filteredGlobalFilters, + globalFilters, appFilters: newAppFilters, }); this.handleStateUpdate(newFilters); diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 6b5fd41dc06ea..528ec4800e7b9 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -88,6 +88,7 @@ class FilterItemUI extends Component { const dataTestSubjDisabled = `filter-${ this.props.filter.meta.disabled ? 'disabled' : 'enabled' }`; + const dataTestSubjPinned = `filter-${isFilterPinned(filter) ? 'pinned' : 'unpinned'}`; const classes = classNames( 'globalFilterItem', @@ -107,7 +108,7 @@ class FilterItemUI extends Component { className={classes} iconOnClick={() => this.props.onRemove()} onClick={this.handleBadgeClick} - data-test-subj={`filter ${dataTestSubjDisabled} ${dataTestSubjKey} ${dataTestSubjValue}`} + data-test-subj={`filter ${dataTestSubjDisabled} ${dataTestSubjKey} ${dataTestSubjValue} ${dataTestSubjPinned}`} /> ); diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js index 6d2a30fa85325..f6089871010c3 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.js +++ b/test/functional/apps/dashboard/dashboard_filter_bar.js @@ -27,6 +27,7 @@ export default function({ getService, getPageObjects }) { const pieChart = getService('pieChart'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); describe('dashboard filter bar', () => { @@ -126,9 +127,46 @@ export default function({ getService, getPageObjects }) { const filterCount = await filterBar.getFilterCount(); expect(filterCount).to.equal(1); + await pieChart.expectPieSliceCount(1); + }); + it("restoring filters doesn't break back button", async () => { + await browser.goBack(); + await PageObjects.dashboard.expectExistsDashboardLandingPage(); + await browser.goForward(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); await pieChart.expectPieSliceCount(1); }); + + it("saving with pinned filter doesn't unpin them", async () => { + const filterKey = 'bytes'; + await filterBar.toggleFilterPinned(filterKey); + await PageObjects.dashboard.switchToEditMode(); + await PageObjects.dashboard.saveDashboard('saved with pinned filters', { + saveAsNew: true, + }); + expect(await filterBar.isFilterPinned(filterKey)).to.be(true); + await pieChart.expectPieSliceCount(1); + }); + + it("navigating to a dashboard with global filter doesn't unpin it if same filter is saved with dashboard", async () => { + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.loadSavedDashboard('with filters'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await filterBar.isFilterPinned('bytes')).to.be(true); + await pieChart.expectPieSliceCount(1); + }); + + it("pinned filters aren't saved", async () => { + await filterBar.removeFilter('bytes'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.loadSavedDashboard('saved with pinned filters'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await filterBar.getFilterCount()).to.be(0); + await pieChart.expectPieSliceCount(5); + }); }); describe('saved search filtering', function() { diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index 9d494b1e6d950..a463a593e9e04 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -32,10 +32,16 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon * @param value filter value * @param enabled filter status */ - public async hasFilter(key: string, value: string, enabled: boolean = true): Promise { + public async hasFilter( + key: string, + value: string, + enabled: boolean = true, + pinned: boolean = false + ): Promise { const filterActivationState = enabled ? 'enabled' : 'disabled'; + const filterPinnedState = pinned ? 'pinned' : 'unpinned'; return testSubjects.exists( - `filter filter-${filterActivationState} filter-key-${key} filter-value-${value}`, + `filter filter-${filterActivationState} filter-key-${key} filter-value-${value} filter-${filterPinnedState}`, { allowHidden: true, } @@ -80,6 +86,11 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } + public async isFilterPinned(key: string): Promise { + const filter = await testSubjects.find(`~filter & ~filter-key-${key}`); + return (await filter.getAttribute('data-test-subj')).includes('filter-pinned'); + } + public async getFilterCount(): Promise { const filters = await testSubjects.findAll('~filter'); return filters.length;