From 4197aff3be50fef9b344a6e93326bf1478d692fa Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Thu, 26 Oct 2023 11:05:19 -0300 Subject: [PATCH] [Saved Search] [Embeddable] Fix issue where Dashboard panel targeting deleted saved search can't be removed (#169896) ## Summary This PR fixes an issue where an undefined reference error makes it so that Dashboard panels targeting deleted saved searches cannot be removed. The bug was likely introduced in 8.10 with embeddable changes in #146849. Fixes #169829. ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [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 - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../embeddable/saved_search_embeddable.tsx | 6 +- .../discover/public/embeddable/types.ts | 2 +- .../view_saved_search_action.test.ts | 2 +- .../embeddable/view_saved_search_action.ts | 6 +- .../panel_actions/get_csv_panel_action.tsx | 7 +- x-pack/test/functional/apps/discover/index.ts | 1 + .../apps/discover/saved_search_embeddable.ts | 109 ++++++++++++++++++ 7 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 x-pack/test/functional/apps/discover/saved_search_embeddable.ts diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 13365af53e33be5..f6299e4596427c8 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -747,11 +747,7 @@ export class SavedSearchEmbeddable } } - public getSavedSearch(): SavedSearch { - if (!this.savedSearch) { - throw new Error('Saved search not defined'); - } - + public getSavedSearch(): SavedSearch | undefined { return this.savedSearch; } diff --git a/src/plugins/discover/public/embeddable/types.ts b/src/plugins/discover/public/embeddable/types.ts index 7fe8a32665783fa..8ca9611cb1703ef 100644 --- a/src/plugins/discover/public/embeddable/types.ts +++ b/src/plugins/discover/public/embeddable/types.ts @@ -22,7 +22,7 @@ export interface SearchOutput extends EmbeddableOutput { } export interface ISearchEmbeddable extends IEmbeddable { - getSavedSearch(): SavedSearch; + getSavedSearch(): SavedSearch | undefined; hasTimeRange(): boolean; } diff --git a/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts b/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts index 0f9b1698c54e9ba..667ded0ba28e80c 100644 --- a/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts +++ b/src/plugins/discover/public/embeddable/view_saved_search_action.test.ts @@ -82,7 +82,7 @@ describe('view saved search action', () => { expect(discoverServiceMock.locator.navigate).toHaveBeenCalledWith( getDiscoverLocatorParams({ input: embeddable.getInput(), - savedSearch: embeddable.getSavedSearch(), + savedSearch: embeddable.getSavedSearch()!, }) ); }); diff --git a/src/plugins/discover/public/embeddable/view_saved_search_action.ts b/src/plugins/discover/public/embeddable/view_saved_search_action.ts index 75cf0971c1481d6..1e5044707ce1d7e 100644 --- a/src/plugins/discover/public/embeddable/view_saved_search_action.ts +++ b/src/plugins/discover/public/embeddable/view_saved_search_action.ts @@ -33,9 +33,13 @@ export class ViewSavedSearchAction implements Action { async execute(context: ActionExecutionContext): Promise { const embeddable = context.embeddable as SavedSearchEmbeddable; + const savedSearch = embeddable.getSavedSearch(); + if (!savedSearch) { + return; + } const locatorParams = getDiscoverLocatorParams({ input: embeddable.getInput(), - savedSearch: embeddable.getSavedSearch(), + savedSearch, }); await this.locator.navigate(locatorParams); } diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 900fbf0994a3e8a..497d52b94444009 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -104,7 +104,7 @@ export class ReportingCsvPanelAction implements ActionDefinition } const savedSearch = embeddable.getSavedSearch(); - const query = savedSearch.searchSource.getField('query'); + const query = savedSearch?.searchSource.getField('query'); // using isOfAggregateQueryType(query) added increased the bundle size over the configured limit of 55.7KB if (query && Boolean(query && 'sql' in query)) { @@ -121,11 +121,12 @@ export class ReportingCsvPanelAction implements ActionDefinition throw new IncompatibleActionError(); } - if (this.isDownloading) { + const savedSearch = embeddable.getSavedSearch(); + + if (!savedSearch || this.isDownloading) { return; } - const savedSearch = embeddable.getSavedSearch(); const { columns, getSearchSource } = await this.getSharingData(savedSearch); const immediateJobParams = this.apiClient.getDecoratedJobParams({ diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts index 9f277a85551cd30..a07eb9c66323991 100644 --- a/x-pack/test/functional/apps/discover/index.ts +++ b/x-pack/test/functional/apps/discover/index.ts @@ -19,5 +19,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./visualize_field')); loadTestFile(require.resolve('./value_suggestions')); loadTestFile(require.resolve('./value_suggestions_non_timebased')); + loadTestFile(require.resolve('./saved_search_embeddable')); }); } diff --git a/x-pack/test/functional/apps/discover/saved_search_embeddable.ts b/x-pack/test/functional/apps/discover/saved_search_embeddable.ts new file mode 100644 index 000000000000000..fd546315a1fd947 --- /dev/null +++ b/x-pack/test/functional/apps/discover/saved_search_embeddable.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); + const dataGrid = getService('dataGrid'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const filterBar = getService('filterBar'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'timePicker', 'discover']); + + describe('discover saved search embeddable', () => { + before(async () => { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/dashboard/current/data'); + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.importExport.load( + 'test/functional/fixtures/kbn_archiver/dashboard/current/kibana' + ); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.setTime({ + from: 'Sep 22, 2015 @ 00:00:00.000', + to: 'Sep 23, 2015 @ 00:00:00.000', + }); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await PageObjects.common.unsetTime(); + }); + + beforeEach(async () => { + await PageObjects.dashboard.navigateToApp(); + await filterBar.ensureFieldEditorModalIsClosed(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + const addSearchEmbeddableToDashboard = async (title = 'Rendering-Test:-saved-search') => { + await dashboardAddPanel.addSavedSearch(title); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + const rows = await dataGrid.getDocTableRows(); + expect(rows.length).to.be.above(0); + }; + + const refreshDashboardPage = async (requireRenderComplete = false) => { + await browser.refresh(); + await PageObjects.header.waitUntilLoadingHasFinished(); + if (requireRenderComplete) { + await PageObjects.dashboard.waitForRenderComplete(); + } + }; + + it('should allow removing the dashboard panel after the underlying saved search has been deleted', async () => { + const searchTitle = 'TempSearch'; + const searchId = '90943e30-9a47-11e8-b64d-95841ca0b247'; + await kibanaServer.savedObjects.create({ + type: 'search', + id: searchId, + overwrite: false, + attributes: { + title: searchTitle, + description: '', + columns: ['agent', 'bytes', 'clientip'], + sort: [['@timestamp', 'desc']], + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"highlightAll":true,"version":true,"query":{"language":"lucene","query":""},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', + }, + }, + references: [ + { + id: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + }, + ], + }); + await addSearchEmbeddableToDashboard(searchTitle); + await PageObjects.dashboard.saveDashboard('Dashboard with deleted saved search', { + waitDialogIsClosed: true, + exitFromEditMode: false, + }); + await kibanaServer.savedObjects.delete({ + type: 'search', + id: searchId, + }); + await refreshDashboardPage(); + await testSubjects.existOrFail('embeddableError'); + const panels = await PageObjects.dashboard.getDashboardPanels(); + await dashboardPanelActions.removePanel(panels[0]); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.missingOrFail('embeddableError'); + }); + }); +}