From bb3da54d3e0774395b1cbaa68bfd0def7447e9c0 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Thu, 23 Sep 2021 14:00:47 +0300 Subject: [PATCH 01/18] [Discover] Step 2 - remove SavedObjectLoader --- src/plugins/discover/kibana.json | 2 +- .../discover/public/__mocks__/saved_search.ts | 34 ----- .../discover/public/__mocks__/services.ts | 2 - .../apps/context/services/context.ts | 11 +- .../components/layout/discover_documents.tsx | 4 +- .../components/layout/discover_layout.tsx | 7 ++ .../components/top_nav/on_save_search.tsx | 32 ++--- .../apps/main/discover_main_route.tsx | 44 ++++--- .../apps/main/services/discover_state.test.ts | 2 +- .../apps/main/services/use_discover_state.ts | 28 +++-- .../apps/main/services/use_saved_search.ts | 4 +- .../application/apps/main/utils/fetch_all.ts | 6 +- .../apps/main/utils/fetch_chart.ts | 6 +- .../apps/main/utils/fetch_documents.ts | 4 +- .../apps/main/utils/fetch_total_hits.ts | 4 +- .../main/utils/get_chart_agg_config.test.ts | 4 +- .../apps/main/utils/get_chart_agg_configs.ts | 4 +- .../apps/main/utils/get_dimensions.test.ts | 4 +- .../apps/main/utils/get_sharing_data.ts | 2 +- .../apps/main/utils/get_state_defaults.ts | 9 +- .../apps/main/utils/persist_saved_search.ts | 7 +- .../apps/main/utils/resolve_index_pattern.ts | 4 +- .../main/utils/update_search_source.test.ts | 2 +- .../apps/main/utils/update_search_source.ts | 2 +- .../helpers/update_search_source.test.ts | 2 +- .../embeddable/saved_search_embeddable.tsx | 5 +- .../embeddable/search_embeddable_factory.ts | 19 +-- .../public/application/embeddable/types.ts | 2 +- .../embeddable/view_saved_search_action.ts | 3 +- src/plugins/discover/public/build_services.ts | 14 +-- src/plugins/discover/public/index.ts | 11 +- src/plugins/discover/public/mocks.ts | 4 +- src/plugins/discover/public/plugin.tsx | 18 ++- .../public/saved_searches/constants.ts | 10 ++ .../saved_searches/get_saved_searches.test.ts | 113 +++++++++++++++++ .../saved_searches/get_saved_searches.ts | 78 ++++++++++++ .../discover/public/saved_searches/index.ts | 17 ++- .../{ => legacy}/_saved_search.ts | 11 +- .../public/saved_searches/legacy/index.ts | 10 ++ .../{ => legacy}/saved_searches.ts | 8 +- .../public/saved_searches/legacy/types.ts | 34 +++++ .../save_saved_searches.test.ts | 117 ++++++++++++++++++ .../saved_searches/save_saved_searches.ts | 78 ++++++++++++ .../saved_search_alias_match_redirect.test.ts | 65 ++++++++++ .../saved_search_alias_match_redirect.ts | 48 +++++++ ...saved_search_url_conflict_callout.test.tsx | 52 ++++++++ .../saved_search_url_conflict_callout.ts | 45 +++++++ .../saved_searches_utils.test.ts | 117 ++++++++++++++++++ .../saved_searches/saved_searches_utils.ts | 43 +++++++ .../discover/public/saved_searches/types.ts | 48 ++++--- src/plugins/discover/tsconfig.json | 3 +- src/plugins/vis_default_editor/kibana.json | 2 +- .../public/components/sidebar/sidebar.tsx | 4 +- .../components/sidebar/sidebar_title.tsx | 8 +- src/plugins/visualizations/public/plugin.ts | 9 +- .../public/saved_visualizations/_saved_vis.ts | 6 - src/plugins/visualizations/public/services.ts | 4 - src/plugins/visualizations/public/vis.ts | 19 +-- .../visualize/public/application/types.ts | 18 ++- .../utils/get_visualization_instance.test.ts | 24 ++-- .../utils/get_visualization_instance.ts | 14 +-- .../get_csv_panel_action.test.ts | 4 +- .../panel_actions/get_csv_panel_action.tsx | 2 +- .../public/__mocks__/shared_imports.ts | 2 +- .../app/hooks/use_search_items/common.ts | 7 -- .../use_search_items/use_search_items.ts | 16 +-- .../transform/public/shared_imports.ts | 2 +- 67 files changed, 1087 insertions(+), 257 deletions(-) create mode 100644 src/plugins/discover/public/saved_searches/constants.ts create mode 100644 src/plugins/discover/public/saved_searches/get_saved_searches.test.ts create mode 100644 src/plugins/discover/public/saved_searches/get_saved_searches.ts rename src/plugins/discover/public/saved_searches/{ => legacy}/_saved_search.ts (80%) create mode 100644 src/plugins/discover/public/saved_searches/legacy/index.ts rename src/plugins/discover/public/saved_searches/{ => legacy}/saved_searches.ts (78%) create mode 100644 src/plugins/discover/public/saved_searches/legacy/types.ts create mode 100644 src/plugins/discover/public/saved_searches/save_saved_searches.test.ts create mode 100644 src/plugins/discover/public/saved_searches/save_saved_searches.ts create mode 100644 src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts create mode 100644 src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts create mode 100644 src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx create mode 100644 src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts create mode 100644 src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts create mode 100644 src/plugins/discover/public/saved_searches/saved_searches_utils.ts diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 46eeb5af1470d..09557ecddea6a 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -15,7 +15,7 @@ "savedObjects", "indexPatternFieldEditor" ], - "optionalPlugins": ["home", "share", "usageCollection"], + "optionalPlugins": ["home", "share", "usageCollection", "spaces"], "requiredBundles": ["kibanaUtils", "home", "kibanaReact", "fieldFormats"], "extraPublicDirs": ["common"], "owner": { diff --git a/src/plugins/discover/public/__mocks__/saved_search.ts b/src/plugins/discover/public/__mocks__/saved_search.ts index ebe65a5770356..a488fe7e04c50 100644 --- a/src/plugins/discover/public/__mocks__/saved_search.ts +++ b/src/plugins/discover/public/__mocks__/saved_search.ts @@ -13,44 +13,10 @@ import { indexPatternWithTimefieldMock } from './index_pattern_with_timefield'; export const savedSearchMock = { id: 'the-saved-search-id', - type: 'search', - attributes: { - title: 'the-saved-search-title', - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"highlightAll":true,"version":true,"query":{"query":"foo : \\"bar\\" ","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', - }, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: 'the-index-pattern-id', - }, - ], - migrationVersion: { search: '7.5.0' }, - error: undefined, searchSource: createSearchSourceMock({ index: indexPatternMock }), } as unknown as SavedSearch; export const savedSearchMockWithTimeField = { id: 'the-saved-search-id-with-timefield', - type: 'search', - attributes: { - title: 'the-saved-search-title', - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"highlightAll":true,"version":true,"query":{"query":"foo : \\"bar\\" ","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', - }, - }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: 'the-index-pattern-id', - }, - ], - migrationVersion: { search: '7.5.0' }, - error: undefined, searchSource: createSearchSourceMock({ index: indexPatternWithTimefieldMock }), } as unknown as SavedSearch; diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index 30d66b113e528..8cc5ccf5aa121 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -16,7 +16,6 @@ import { SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING, } from '../../common'; -import { savedSearchMock } from './saved_search'; import { UI_SETTINGS } from '../../../data/common'; import { TopNavMenu } from '../../../navigation/public'; import { FORMATS_UI_SETTINGS } from 'src/plugins/field_formats/common'; @@ -78,7 +77,6 @@ export const discoverServiceMock = { editIndexPattern: jest.fn(), }, }, - getSavedSearchById: (id?: string) => Promise.resolve(savedSearchMock), navigation: { ui: { TopNavMenu }, }, diff --git a/src/plugins/discover/public/application/apps/context/services/context.ts b/src/plugins/discover/public/application/apps/context/services/context.ts index 237de8e52e656..f7fd65cda44c5 100644 --- a/src/plugins/discover/public/application/apps/context/services/context.ts +++ b/src/plugins/discover/public/application/apps/context/services/context.ts @@ -5,7 +5,12 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { Filter, IndexPattern, IndexPatternsContract, SearchSource } from 'src/plugins/data/public'; +import { + Filter, + IndexPattern, + IndexPatternsContract, + ISearchSource, +} from 'src/plugins/data/public'; import { reverseSortDir, SortDirection } from './utils/sorting'; import { convertIsoToMillis, extractNanos } from './utils/date_conversion'; import { fetchHitsInInterval } from './utils/fetch_hits_in_interval'; @@ -58,7 +63,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields } const indexPattern = await indexPatterns.get(indexPatternId); const { data } = getServices(); - const searchSource = data.search.searchSource.createEmpty() as SearchSource; + const searchSource = data.search.searchSource.createEmpty(); updateSearchSource(searchSource, indexPattern, filters, Boolean(useNewFieldsApi)); const sortDirToApply = type === SurrDocType.SUCCESSORS ? sortDir : reverseSortDir(sortDir); @@ -110,7 +115,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields } export function updateSearchSource( - searchSource: SearchSource, + searchSource: ISearchSource, indexPattern: IndexPattern, filters: Filter[], useNewFieldsApi: boolean diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx index e0e0c9c6f8831..d6ede9aa7fe5f 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx @@ -134,7 +134,7 @@ function DiscoverDocumentsComponent({ sort={state.sort || []} isLoading={isLoading} searchDescription={savedSearch.description} - sharedItemTitle={savedSearch.lastSavedTitle} + sharedItemTitle={savedSearch.title} onAddColumn={onAddColumn} onFilter={onAddFilter as DocViewFilterFn} onMoveColumn={onMoveColumn} @@ -156,7 +156,7 @@ function DiscoverDocumentsComponent({ sort={(state.sort as SortPairArr[]) || []} sampleSize={sampleSize} searchDescription={savedSearch.description} - searchTitle={savedSearch.lastSavedTitle} + searchTitle={savedSearch.title} setExpandedDoc={setExpandedDoc} showTimeCol={showTimeCol} services={services} diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index 4bbef32dcbadd..03b055d2ea847 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -39,6 +39,10 @@ import { useDataGridColumns } from '../../../../helpers/use_data_grid_columns'; import { DiscoverDocuments } from './discover_documents'; import { FetchStatus } from '../../../../types'; import { useDataState } from '../../utils/use_data_state'; +import { + SavedSearchURLConflictCallout, + useSavedSearchAliasMatchRedirect, +} from '../../../../../saved_searches'; /** * Local storage key for sidebar persistence state @@ -80,6 +84,8 @@ export function DiscoverLayout({ } }, [dataState.fetchStatus]); + useSavedSearchAliasMatchRedirect({ savedSearch, spaces: services.spaces }); + const timeField = useMemo(() => { return indexPattern.type !== 'rollup' ? indexPattern.timeFieldName : undefined; }, [indexPattern]); @@ -174,6 +180,7 @@ export function DiscoverLayout({ resetSavedSearch={resetSavedSearch} /> +

{savedSearch.title}

diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx index c68b1ab7b844c..9418783592bf6 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { SavedObjectSaveModal, showSaveModal } from '../../../../../../../saved_objects/public'; -import { SavedSearch } from '../../../../../saved_searches'; +import { SavedSearch, SaveSavedSearchOptions } from '../../../../../saved_searches'; import { IndexPattern } from '../../../../../../../data/common'; import { DiscoverServices } from '../../../../../build_services'; import { GetStateReturn } from '../../services/discover_state'; @@ -27,11 +27,7 @@ async function saveDataSource({ indexPattern: IndexPattern; navigateTo: (url: string) => void; savedSearch: SavedSearch; - saveOptions: { - confirmOverwrite: boolean; - isTitleDuplicateConfirmed: boolean; - onTitleDuplicate: () => void; - }; + saveOptions: SaveSavedSearchOptions; services: DiscoverServices; state: GetStateReturn; }) { @@ -47,14 +43,20 @@ async function saveDataSource({ }), 'data-test-subj': 'saveSearchSuccess', }); - - if (savedSearch.id !== prevSavedSearchId) { - navigateTo(`/view/${encodeURIComponent(savedSearch.id)}`); + if (id !== prevSavedSearchId) { + navigateTo(`/view/${encodeURIComponent(id)}`); } else { // Update defaults so that "reload saved query" functions correctly state.resetAppState(); - services.chrome.docTitle.change(savedSearch.lastSavedTitle!); - setBreadcrumbsTitle(savedSearch, services.chrome); + services.chrome.docTitle.change(savedSearch.title!); + + setBreadcrumbsTitle( + { + ...savedSearch, + id: prevSavedSearchId ?? id, + }, + services.chrome + ); } } } @@ -106,11 +108,9 @@ export async function onSaveSearch({ }) => { const currentTitle = savedSearch.title; savedSearch.title = newTitle; - savedSearch.copyOnSave = newCopyOnSave; - const saveOptions = { - confirmOverwrite: false, - isTitleDuplicateConfirmed, + const saveOptions: SaveSavedSearchOptions = { onTitleDuplicate, + copyOnSave: newCopyOnSave, }; const response = await saveDataSource({ indexPattern, @@ -133,7 +133,7 @@ export async function onSaveSearch({ {}} - title={savedSearch.title} + title={savedSearch.title ?? ''} showCopyOnSave={!!savedSearch.id} objectType={i18n.translate('discover.localMenu.saveSaveSearchObjectType', { defaultMessage: 'search', diff --git a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx index 5141908e44ade..d7580ccf7b92a 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx @@ -8,10 +8,9 @@ import React, { useEffect, useState, memo } from 'react'; import { History } from 'history'; import { useParams } from 'react-router-dom'; -import type { SavedObject as SavedObjectDeprecated } from 'src/plugins/saved_objects/public'; -import { IndexPatternAttributes, SavedObject } from 'src/plugins/data/common'; +import { IndexPatternAttributes, ISearchSource, SavedObject } from 'src/plugins/data/common'; import { DiscoverServices } from '../../../build_services'; -import { SavedSearch } from '../../../saved_searches'; +import { SavedSearch, getSavedSearch, getSavedSearchFullPathUrl } from '../../../saved_searches'; import { getState } from './services/discover_state'; import { loadIndexPattern, resolveIndexPattern } from './utils/resolve_index_pattern'; import { DiscoverMainApp } from './discover_main_app'; @@ -58,18 +57,16 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { useEffect(() => { const savedSearchId = id; - async function loadDefaultOrCurrentIndexPattern(usedSavedSearch: SavedSearch) { + async function loadDefaultOrCurrentIndexPattern(searchSource: ISearchSource) { await data.indexPatterns.ensureDefaultDataView(); const { appStateContainer } = getState({ history, uiSettings: config }); const { index } = appStateContainer.getState(); const ip = await loadIndexPattern(index || '', data.indexPatterns, config); const ipList = ip.list as Array>; - const indexPatternData = await resolveIndexPattern( - ip, - usedSavedSearch.searchSource, - toastNotifications - ); + const indexPatternData = await resolveIndexPattern(ip, searchSource, toastNotifications); + setIndexPatternList(ipList); + return indexPatternData; } @@ -77,17 +74,27 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { try { // force a refresh if a given saved search without id was saved setSavedSearch(undefined); - const loadedSavedSearch = await services.getSavedSearchById(savedSearchId); - const loadedIndexPattern = await loadDefaultOrCurrentIndexPattern(loadedSavedSearch); - if (loadedSavedSearch && !loadedSavedSearch?.searchSource.getField('index')) { - loadedSavedSearch.searchSource.setField('index', loadedIndexPattern); + + const currentSavedSearch = await getSavedSearch(savedSearchId, { + search: services.data.search, + savedObjectsClient: core.savedObjects.client, + }); + + const loadedIndexPattern = await loadDefaultOrCurrentIndexPattern( + currentSavedSearch.searchSource + ); + + if (!currentSavedSearch.searchSource.getField('index')) { + currentSavedSearch.searchSource.setField('index', loadedIndexPattern); } - setSavedSearch(loadedSavedSearch); - if (savedSearchId) { + + setSavedSearch(currentSavedSearch); + + if (currentSavedSearch.id) { chrome.recentlyAccessed.add( - (loadedSavedSearch as unknown as SavedObjectDeprecated).getFullPath(), - loadedSavedSearch.title, - loadedSavedSearch.id + getSavedSearchFullPathUrl(currentSavedSearch.id), + currentSavedSearch.title ?? '', + currentSavedSearch.id ); } } catch (e) { @@ -112,6 +119,7 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { loadSavedSearch(); }, [ + core.savedObjects.client, basePath, chrome.recentlyAccessed, config, diff --git a/src/plugins/discover/public/application/apps/main/services/discover_state.test.ts b/src/plugins/discover/public/application/apps/main/services/discover_state.test.ts index 905d81a6fc716..9968ca6f1f63f 100644 --- a/src/plugins/discover/public/application/apps/main/services/discover_state.test.ts +++ b/src/plugins/discover/public/application/apps/main/services/discover_state.test.ts @@ -14,7 +14,7 @@ import { } from './discover_state'; import { createBrowserHistory, History } from 'history'; import { dataPluginMock } from '../../../../../../data/public/mocks'; -import { SavedSearch } from '../../../../saved_searches'; +import type { SavedSearch } from '../../../../saved_searches'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; let history: History; diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts index e11a9937111a1..bdf40bebed193 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -11,7 +11,7 @@ import { History } from 'history'; import { getState } from './discover_state'; import { getStateDefaults } from '../utils/get_state_defaults'; import { DiscoverServices } from '../../../../build_services'; -import { SavedSearch } from '../../../../saved_searches'; +import { SavedSearch, getSavedSearch } from '../../../../saved_searches'; import { loadIndexPattern } from '../utils/resolve_index_pattern'; import { useSavedSearch as useSavedSearchData } from './use_saved_search'; import { @@ -147,16 +147,22 @@ export function useDiscoverState({ */ const resetSavedSearch = useCallback( async (id?: string) => { - const newSavedSearch = await services.getSavedSearchById(id); - const newIndexPattern = newSavedSearch.searchSource.getField('index') || indexPattern; - newSavedSearch.searchSource.setField('index', newIndexPattern); - const newAppState = getStateDefaults({ - config, - data, - savedSearch: newSavedSearch, - }); - await stateContainer.replaceUrlAppState(newAppState); - setState(newAppState); + if (id) { + const newSavedSearch = await getSavedSearch(id, { + search: services.data.search, + savedObjectsClient: services.core.savedObjects.client, + }); + + const newIndexPattern = newSavedSearch.searchSource.getField('index') || indexPattern; + newSavedSearch.searchSource.setField('index', newIndexPattern); + const newAppState = getStateDefaults({ + config, + data, + savedSearch: newSavedSearch, + }); + await stateContainer.replaceUrlAppState(newAppState); + setState(newAppState); + } }, [indexPattern, services, config, data, stateContainer] ); diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts index 164dff8627790..bfdb046f6934b 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.ts @@ -10,7 +10,7 @@ import { BehaviorSubject, merge, Subject } from 'rxjs'; import { debounceTime, filter, tap } from 'rxjs/operators'; import { DiscoverServices } from '../../../../build_services'; import { DiscoverSearchSessionManager } from './discover_search_session'; -import { SearchSource } from '../../../../../../data/common'; +import { ISearchSource } from '../../../../../../data/common'; import { GetStateReturn } from './discover_state'; import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; import { RequestAdapter } from '../../../../../../inspector/public'; @@ -91,7 +91,7 @@ export const useSavedSearch = ({ }: { initialFetchStatus: FetchStatus; searchSessionManager: DiscoverSearchSessionManager; - searchSource: SearchSource; + searchSource: ISearchSource; services: DiscoverServices; stateContainer: GetStateReturn; useNewFieldsApi: boolean; diff --git a/src/plugins/discover/public/application/apps/main/utils/fetch_all.ts b/src/plugins/discover/public/application/apps/main/utils/fetch_all.ts index 53d13ee547b0f..e9d9335abcda0 100644 --- a/src/plugins/discover/public/application/apps/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/apps/main/utils/fetch_all.ts @@ -14,11 +14,11 @@ import { sendResetMsg, } from '../services/use_saved_search_messages'; import { updateSearchSource } from './update_search_source'; -import { SortOrder } from '../../../../saved_searches/types'; +import type { SortOrder } from '../../../../saved_searches'; import { fetchDocuments } from './fetch_documents'; import { fetchTotalHits } from './fetch_total_hits'; import { fetchChart } from './fetch_chart'; -import { SearchSource } from '../../../../../../data/common'; +import { ISearchSource } from '../../../../../../data/common'; import { Adapters } from '../../../../../../inspector'; import { AppState } from '../services/discover_state'; import { FetchStatus } from '../../../types'; @@ -29,7 +29,7 @@ import { ReduxLikeStateContainer } from '../../../../../../kibana_utils/common'; export function fetchAll( dataSubjects: SavedSearchData, - searchSource: SearchSource, + searchSource: ISearchSource, reset = false, fetchDeps: { abortController: AbortController; diff --git a/src/plugins/discover/public/application/apps/main/utils/fetch_chart.ts b/src/plugins/discover/public/application/apps/main/utils/fetch_chart.ts index 67f34c7503c59..50f3a1b8bfea7 100644 --- a/src/plugins/discover/public/application/apps/main/utils/fetch_chart.ts +++ b/src/plugins/discover/public/application/apps/main/utils/fetch_chart.ts @@ -11,7 +11,7 @@ import { DataPublicPluginStart, isCompleteResponse, search, - SearchSource, + ISearchSource, } from '../../../../../../data/public'; import { Adapters } from '../../../../../../inspector'; import { getChartAggConfigs, getDimensions } from './index'; @@ -25,7 +25,7 @@ import { sendErrorMsg, sendLoadingMsg } from '../services/use_saved_search_messa export function fetchChart( data$: SavedSearchData, - searchSource: SearchSource, + searchSource: ISearchSource, { abortController, appStateContainer, @@ -114,7 +114,7 @@ export function fetchChart( } export function updateSearchSource( - searchSource: SearchSource, + searchSource: ISearchSource, interval: string, data: DataPublicPluginStart ) { diff --git a/src/plugins/discover/public/application/apps/main/utils/fetch_documents.ts b/src/plugins/discover/public/application/apps/main/utils/fetch_documents.ts index 2f06a9dbbb3db..6c5eff7cff702 100644 --- a/src/plugins/discover/public/application/apps/main/utils/fetch_documents.ts +++ b/src/plugins/discover/public/application/apps/main/utils/fetch_documents.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { filter } from 'rxjs/operators'; import { Adapters } from '../../../../../../inspector/common'; -import { isCompleteResponse, SearchSource } from '../../../../../../data/common'; +import { isCompleteResponse, ISearchSource } from '../../../../../../data/common'; import { FetchStatus } from '../../../types'; import { SavedSearchData } from '../services/use_saved_search'; import { sendErrorMsg, sendLoadingMsg } from '../services/use_saved_search_messages'; @@ -17,7 +17,7 @@ import { DiscoverServices } from '../../../../build_services'; export const fetchDocuments = ( data$: SavedSearchData, - searchSource: SearchSource, + searchSource: ISearchSource, { abortController, inspectorAdapters, diff --git a/src/plugins/discover/public/application/apps/main/utils/fetch_total_hits.ts b/src/plugins/discover/public/application/apps/main/utils/fetch_total_hits.ts index 9688f5ddd614d..cfab0d17fcd54 100644 --- a/src/plugins/discover/public/application/apps/main/utils/fetch_total_hits.ts +++ b/src/plugins/discover/public/application/apps/main/utils/fetch_total_hits.ts @@ -11,7 +11,7 @@ import { filter } from 'rxjs/operators'; import { DataPublicPluginStart, isCompleteResponse, - SearchSource, + ISearchSource, } from '../../../../../../data/public'; import { Adapters } from '../../../../../../inspector/common'; import { FetchStatus } from '../../../types'; @@ -20,7 +20,7 @@ import { sendErrorMsg, sendLoadingMsg } from '../services/use_saved_search_messa export function fetchTotalHits( data$: SavedSearchData, - searchSource: SearchSource, + searchSource: ISearchSource, { abortController, data, diff --git a/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_config.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_config.test.ts index 3eef49fe6ddcb..515565f0062c9 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_config.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_config.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; -import { SearchSource } from '../../../../../../data/public'; +import { ISearchSource } from '../../../../../../data/public'; import { dataPluginMock } from '../../../../../../data/public/mocks'; import { getChartAggConfigs } from './get_chart_agg_configs'; @@ -22,7 +22,7 @@ describe('getChartAggConfigs', () => { } }, removeField: jest.fn(), - } as unknown as SearchSource; + } as unknown as ISearchSource; const dataMock = dataPluginMock.createStartContract(); diff --git a/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_configs.ts b/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_configs.ts index 2665254027fd9..65f98f72beec0 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_configs.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_chart_agg_configs.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { SearchSource } from '../../../../../../data/common'; +import { ISearchSource } from '../../../../../../data/common'; import { DataPublicPluginStart } from '../../../../../../data/public'; /** @@ -13,7 +13,7 @@ import { DataPublicPluginStart } from '../../../../../../data/public'; * for Discover's histogram vis */ export function getChartAggConfigs( - searchSource: SearchSource, + searchSource: ISearchSource, histogramInterval: string, data: DataPublicPluginStart ) { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_dimensions.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_dimensions.test.ts index b98662f2db3b5..35a6e955fe5b2 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_dimensions.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_dimensions.test.ts @@ -9,7 +9,7 @@ import { dataPluginMock } from '../../../../../../data/public/mocks'; import { getDimensions } from './get_dimensions'; import { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; -import { SearchSource, calculateBounds } from '../../../../../../data/common'; +import { ISearchSource, calculateBounds } from '../../../../../../data/common'; import { getChartAggConfigs } from './get_chart_agg_configs'; test('getDimensions', () => { @@ -23,7 +23,7 @@ test('getDimensions', () => { return indexPattern; } }, - } as unknown as SearchSource; + } as unknown as ISearchSource; const dataMock = dataPluginMock.createStartContract(); dataMock.query.timefilter.timefilter.getTime = () => { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts index 420ff0fa11eeb..33bf61c194d9b 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts @@ -11,7 +11,7 @@ import type { IUiSettingsClient } from 'src/core/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; import type { ISearchSource } from 'src/plugins/data/common'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; -import type { SavedSearch, SortOrder } from '../../../../saved_searches/types'; +import type { SavedSearch, SortOrder } from '../../../../saved_searches'; import { getSortForSearchSource } from '../components/doc_table'; import { AppState } from '../services/discover_state'; diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts index 4061d9a61f0a3..ae9a97c4385c4 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts @@ -31,10 +31,11 @@ export function getStateDefaults({ data: DataPublicPluginStart; savedSearch: SavedSearch; }) { - const searchSource = savedSearch.searchSource; - const indexPattern = savedSearch.searchSource.getField('index'); + const { searchSource } = savedSearch; + const indexPattern = searchSource.getField('index'); + const query = searchSource.getField('query') || data.query.queryString.getDefaultQuery(); - const sort = getSortArray(savedSearch.sort, indexPattern!); + const sort = getSortArray(savedSearch.sort ?? [], indexPattern!); const columns = getDefaultColumns(savedSearch, config); const defaultState = { @@ -43,7 +44,7 @@ export function getStateDefaults({ ? getDefaultSort(indexPattern, config.get(SORT_DEFAULT_ORDER_SETTING, 'desc')) : sort, columns, - index: indexPattern!.id, + index: indexPattern?.id, interval: 'auto', filters: cloneDeep(searchSource.getOwnField('filter')), hideChart: undefined, diff --git a/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts b/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts index a5e1e2bb6c2ea..d42f5595489a9 100644 --- a/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts +++ b/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts @@ -10,9 +10,10 @@ import { updateSearchSource } from './update_search_source'; import { IndexPattern } from '../../../../../../data/public'; import { SavedSearch } from '../../../../saved_searches'; import { AppState } from '../services/discover_state'; -import { SortOrder } from '../../../../saved_searches/types'; +import type { SortOrder } from '../../../../saved_searches'; import { SavedObjectSaveOpts } from '../../../../../../saved_objects/public'; import { DiscoverServices } from '../../../../build_services'; +import { saveSavedSearch } from '../../../../saved_searches'; /** * Helper function to update and persist the given savedSearch @@ -52,8 +53,8 @@ export async function persistSavedSearch( } try { - const id = await savedSearch.save(saveOptions); - onSuccess(id); + const id = await saveSavedSearch(savedSearch, saveOptions, services.core.savedObjects.client); + onSuccess(id!); return { id }; } catch (saveError) { onError(saveError, savedSearch); diff --git a/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts b/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts index d30b67db31186..613615446ee08 100644 --- a/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts +++ b/src/plugins/discover/public/application/apps/main/utils/resolve_index_pattern.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import type { IndexPattern, IndexPatternsContract, SearchSource } from 'src/plugins/data/common'; +import type { IndexPattern, IndexPatternsContract, ISearchSource } from 'src/plugins/data/common'; import type { IUiSettingsClient, SavedObject, ToastsStart } from 'kibana/public'; export type IndexPatternSavedObject = SavedObject & { title: string }; @@ -95,7 +95,7 @@ export async function loadIndexPattern( */ export function resolveIndexPattern( ip: IndexPatternData, - searchSource: SearchSource, + searchSource: ISearchSource, toastNotifications: ToastsStart ) { const { loaded: loadedIndexPattern, stateVal, stateValFound } = ip; diff --git a/src/plugins/discover/public/application/apps/main/utils/update_search_source.test.ts b/src/plugins/discover/public/application/apps/main/utils/update_search_source.test.ts index 945140e0586ab..22f3b6ad86f6c 100644 --- a/src/plugins/discover/public/application/apps/main/utils/update_search_source.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/update_search_source.test.ts @@ -9,7 +9,7 @@ import { updateSearchSource } from './update_search_source'; import { createSearchSourceMock } from '../../../../../../data/common/search/search_source/mocks'; import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import { SortOrder } from '../../../../saved_searches/types'; +import type { SortOrder } from '../../../../saved_searches'; import { discoverServiceMock } from '../../../../__mocks__/services'; describe('updateSearchSource', () => { diff --git a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts index 4dfcbc7b79712..6d592e176afe5 100644 --- a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts +++ b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts @@ -8,7 +8,7 @@ import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; import { IndexPattern, ISearchSource } from '../../../../../../data/common'; -import { SortOrder } from '../../../../saved_searches/types'; +import type { SortOrder } from '../../../../saved_searches'; import { DiscoverServices } from '../../../../build_services'; import { getSortForSearchSource } from '../components/doc_table'; diff --git a/src/plugins/discover/public/application/embeddable/helpers/update_search_source.test.ts b/src/plugins/discover/public/application/embeddable/helpers/update_search_source.test.ts index f3edc523f4464..f09131cb5c926 100644 --- a/src/plugins/discover/public/application/embeddable/helpers/update_search_source.test.ts +++ b/src/plugins/discover/public/application/embeddable/helpers/update_search_source.test.ts @@ -8,7 +8,7 @@ import { createSearchSourceMock } from '../../../../../data/common/search/search_source/mocks'; import { updateSearchSource } from './update_search_source'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; -import { SortOrder } from '../../../saved_searches/types'; +import type { SortOrder } from '../../../saved_searches'; describe('updateSearchSource', () => { const defaults = { diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx index ef8670f976672..8849806cf5959 100644 --- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx @@ -165,7 +165,7 @@ export class SavedSearchEmbeddable const executionContext = { type: this.type, name: 'discover', - id: this.savedSearch.id, + id: this.savedSearch.id!, description: this.output.title || this.output.defaultTitle || '', url: this.output.editUrl, parent: this.input.executionContext, @@ -232,7 +232,7 @@ export class SavedSearchEmbeddable searchDescription: this.savedSearch.description, description: this.savedSearch.description, inspectorAdapters: this.inspectorAdapters, - searchTitle: this.savedSearch.lastSavedTitle, + searchTitle: this.savedSearch.title, services: this.services, onAddColumn: (columnName: string) => { if (!props.columns) { @@ -404,7 +404,6 @@ export class SavedSearchEmbeddable public destroy() { super.destroy(); - this.savedSearch.destroy(); if (this.searchProps) { delete this.searchProps; } diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts index 95f5b2d3ce284..645e3c5c67357 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts @@ -20,6 +20,7 @@ import { TimeRange } from '../../../../data/public'; import { SearchInput, SearchOutput } from './types'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; import { SavedSearchEmbeddable } from './saved_search_embeddable'; +import { getSavedSearch, getSavedSearchUrl } from '../../saved_searches'; interface StartServices { executeTriggerActions: UiActionsStart['executeTriggerActions']; @@ -59,20 +60,24 @@ export class SearchEmbeddableFactory input: Partial & { id: string; timeRange: TimeRange }, parent?: Container ): Promise => { - const filterManager = getServices().filterManager; - - const url = await getServices().getSavedSearchUrlById(savedObjectId); - const editUrl = getServices().addBasePath(`/app/discover${url}`); + const services = getServices(); + const filterManager = services.filterManager; + const url = getSavedSearchUrl(savedObjectId); + const editUrl = services.addBasePath(`/app/discover${url}`); try { - const savedObject = await getServices().getSavedSearchById(savedObjectId); - const indexPattern = savedObject.searchSource.getField('index'); + const savedSearch = await getSavedSearch(savedObjectId, { + search: services.data.search, + savedObjectsClient: services.core.savedObjects.client, + }); + + const indexPattern = savedSearch.searchSource.getField('index'); const { executeTriggerActions } = await this.getStartServices(); const { SavedSearchEmbeddable: SavedSearchEmbeddableClass } = await import( './saved_search_embeddable' ); return new SavedSearchEmbeddableClass( { - savedSearch: savedObject, + savedSearch, editUrl, editPath: url, filterManager, diff --git a/src/plugins/discover/public/application/embeddable/types.ts b/src/plugins/discover/public/application/embeddable/types.ts index 5a08534918d4f..de109e3fa7879 100644 --- a/src/plugins/discover/public/application/embeddable/types.ts +++ b/src/plugins/discover/public/application/embeddable/types.ts @@ -13,7 +13,7 @@ import { IEmbeddable, } from 'src/plugins/embeddable/public'; import { Filter, IndexPattern, TimeRange, Query } from '../../../../data/public'; -import { SavedSearch } from '../..'; +import { SavedSearch } from '../../saved_searches'; import { SortOrder } from '../apps/main/components/doc_table/components/table_header/helpers'; export interface SearchInput extends EmbeddableInput { diff --git a/src/plugins/discover/public/application/embeddable/view_saved_search_action.ts b/src/plugins/discover/public/application/embeddable/view_saved_search_action.ts index 69c273f326c61..e4b97d011ff64 100644 --- a/src/plugins/discover/public/application/embeddable/view_saved_search_action.ts +++ b/src/plugins/discover/public/application/embeddable/view_saved_search_action.ts @@ -12,6 +12,7 @@ import { IEmbeddable, ViewMode } from '../../../../embeddable/public'; import { Action } from '../../../../ui_actions/public'; import { SavedSearchEmbeddable } from './saved_search_embeddable'; import { SEARCH_EMBEDDABLE_TYPE } from '../../../common'; +import { getSavedSearchUrl } from '../../saved_searches'; export const ACTION_VIEW_SAVED_SEARCH = 'ACTION_VIEW_SAVED_SEARCH'; @@ -28,7 +29,7 @@ export class ViewSavedSearchAction implements Action { async execute(context: ActionExecutionContext): Promise { const { embeddable } = context; const savedSearchId = (embeddable as SavedSearchEmbeddable).getSavedSearch().id; - const path = `#/view/${encodeURIComponent(savedSearchId)}`; + const path = getSavedSearchUrl(savedSearchId); const app = embeddable ? embeddable.getOutput().editApp : undefined; await this.application.navigateToApp(app ? app : 'discover', { path }); } diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index e88f00fadcbf1..ab2484abee892 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -31,13 +31,14 @@ import { UiCounterMetricType } from '@kbn/analytics'; import { Storage } from '../../kibana_utils/public'; import { DiscoverStartPlugins } from './plugin'; -import { createSavedSearchesLoader, SavedSearch } from './saved_searches'; import { getHistory } from './kibana_services'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { UrlForwardingStart } from '../../url_forwarding/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; +import type { SpacesApi } from '../../../../x-pack/plugins/spaces/public'; + export interface DiscoverServices { addBasePath: (path: string) => string; capabilities: Capabilities; @@ -57,13 +58,12 @@ export interface DiscoverServices { urlForwarding: UrlForwardingStart; timefilter: TimefilterContract; toastNotifications: ToastsStart; - getSavedSearchById: (id?: string) => Promise; - getSavedSearchUrlById: (id: string) => Promise; uiSettings: IUiSettingsClient; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; indexPatternFieldEditor: IndexPatternFieldEditorStart; http: HttpStart; storage: Storage; + spaces?: SpacesApi; } export function buildServices( @@ -71,11 +71,6 @@ export function buildServices( plugins: DiscoverStartPlugins, context: PluginInitializerContext ): DiscoverServices { - const services = { - savedObjectsClient: core.savedObjects.client, - savedObjects: plugins.savedObjects, - }; - const savedObjectService = createSavedSearchesLoader(services); const { usageCollection } = plugins; const storage = new Storage(localStorage); @@ -88,8 +83,6 @@ export function buildServices( docLinks: core.docLinks, theme: plugins.charts.theme, filterManager: plugins.data.query.filterManager, - getSavedSearchById: async (id?: string) => savedObjectService.get(id), - getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id), history: getHistory, indexPatterns: plugins.data.indexPatterns, inspector: plugins.inspector, @@ -107,5 +100,6 @@ export function buildServices( trackUiMetric: usageCollection?.reportUiCounter.bind(usageCollection, 'discover'), indexPatternFieldEditor: plugins.indexPatternFieldEditor, http: core.http, + spaces: plugins.spaces, }; } diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 3840df4353faf..5215b67784c88 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -9,12 +9,21 @@ import { PluginInitializerContext } from 'kibana/public'; import { DiscoverPlugin } from './plugin'; +export { + getSavedSearch, + getSavedSearchFullPathUrl, + getSavedSearchUrl, + SavedSearch, + LegacySavedSearch, + SavedSearchLoader, + __LEGACY, +} from './saved_searches'; + export { DiscoverSetup, DiscoverStart } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new DiscoverPlugin(initializerContext); } -export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches'; export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; export { loadSharingDataHelpers } from './shared'; diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts index 71de630132b0a..6a3c703ea0da8 100644 --- a/src/plugins/discover/public/mocks.ts +++ b/src/plugins/discover/public/mocks.ts @@ -24,7 +24,9 @@ const createSetupContract = (): Setup => { const createStartContract = (): Start => { const startContract: Start = { - savedSearchLoader: {} as DiscoverStart['savedSearchLoader'], + __LEGACY: { + savedSearchLoader: {} as DiscoverStart['__LEGACY']['savedSearchLoader'], + }, urlGenerator: { createUrl: jest.fn(), } as unknown as DiscoverStart['urlGenerator'], diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index afb83d6cbd667..e34e7644caa25 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -45,7 +45,7 @@ import { getScopedHistory, syncHistoryLocations, } from './kibana_services'; -import { createSavedSearchesLoader } from './saved_searches'; +import { __LEGACY } from './saved_searches'; import { registerFeature } from './register_feature'; import { buildServices } from './build_services'; import { @@ -61,6 +61,7 @@ import { replaceUrlHashQuery } from '../../kibana_utils/public/'; import { IndexPatternFieldEditorStart } from '../../../plugins/index_pattern_field_editor/public'; import { DeferredSpinner } from './shared'; import { ViewSavedSearchAction } from './application/embeddable/view_saved_search_action'; +import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -120,7 +121,9 @@ export interface DiscoverSetup { } export interface DiscoverStart { - savedSearchLoader: SavedObjectLoader; + __LEGACY: { + savedSearchLoader: SavedObjectLoader; + }; /** * @deprecated Use URL locator instead. URL generator will be removed. @@ -189,6 +192,7 @@ export interface DiscoverStartPlugins { savedObjects: SavedObjectsStart; usageCollection?: UsageCollectionSetup; indexPatternFieldEditor: IndexPatternFieldEditorStart; + spaces?: SpacesPluginStart; } /** @@ -410,10 +414,12 @@ export class DiscoverPlugin return { urlGenerator: this.urlGenerator, locator: this.locator, - savedSearchLoader: createSavedSearchesLoader({ - savedObjectsClient: core.savedObjects.client, - savedObjects: plugins.savedObjects, - }), + __LEGACY: { + savedSearchLoader: __LEGACY.createSavedSearchesLoader({ + savedObjectsClient: core.savedObjects.client, + savedObjects: plugins.savedObjects, + }), + }, }; } diff --git a/src/plugins/discover/public/saved_searches/constants.ts b/src/plugins/discover/public/saved_searches/constants.ts new file mode 100644 index 0000000000000..f8e191c263bd7 --- /dev/null +++ b/src/plugins/discover/public/saved_searches/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** @internal **/ +export const SAVED_SEARCH_TYPE = 'search'; diff --git a/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts b/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts new file mode 100644 index 0000000000000..d0c53f3556f29 --- /dev/null +++ b/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts @@ -0,0 +1,113 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { SavedObjectsStart } from '../../../../core/public'; +import type { DataPublicPluginStart } from '../../../data/public'; + +import { savedObjectsServiceMock } from '../../../../core/public/mocks'; +import { dataPluginMock } from '../../../data/public/mocks'; + +import { getSavedSearch } from './get_saved_searches'; + +describe('getSavedSearch', () => { + let search: DataPublicPluginStart['search']; + let savedObjectsClient: SavedObjectsStart['client']; + + beforeEach(() => { + savedObjectsClient = savedObjectsServiceMock.createStartContract().client; + search = dataPluginMock.createStartContract().search; + }); + + test('should return empty saved search in case of no id', async () => { + const savedSearch = await getSavedSearch(undefined, { savedObjectsClient, search }); + + expect(search.searchSource.createEmpty).toHaveBeenCalled(); + expect(savedSearch).toHaveProperty('searchSource'); + }); + + test('should find saved search', async () => { + savedObjectsClient.resolve = jest.fn().mockReturnValue({ + saved_object: { + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', + }, + title: 'test1', + sort: [['order_date', 'desc']], + columns: ['_source'], + description: 'description', + grid: {}, + hideChart: false, + }, + id: 'ccf1af80-2297-11ec-86e0-1155ffb9c7a7', + type: 'search', + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + type: 'index-pattern', + }, + ], + namespaces: ['default'], + }, + outcome: 'exactMatch', + }); + + const savedSearch = await getSavedSearch('ccf1af80-2297-11ec-86e0-1155ffb9c7a7', { + savedObjectsClient, + search, + }); + + expect(savedObjectsClient.resolve).toHaveBeenCalled(); + expect(savedSearch).toMatchInlineSnapshot(` + Object { + "columns": Array [ + "_source", + ], + "description": "description", + "grid": Object {}, + "hideChart": false, + "id": "ccf1af80-2297-11ec-86e0-1155ffb9c7a7", + "searchSource": Object { + "create": [MockFunction], + "createChild": [MockFunction], + "createCopy": [MockFunction], + "destroy": [MockFunction], + "fetch": [MockFunction], + "fetch$": [MockFunction], + "getField": [MockFunction], + "getFields": [MockFunction], + "getId": [MockFunction], + "getOwnField": [MockFunction], + "getParent": [MockFunction], + "getSearchRequestBody": [MockFunction], + "getSerializedFields": [MockFunction], + "history": Array [], + "onRequestStart": [MockFunction], + "removeField": [MockFunction], + "serialize": [MockFunction], + "setField": [MockFunction], + "setFields": [MockFunction], + "setParent": [MockFunction], + "setPreferredSearchStrategyId": [MockFunction], + }, + "sharingSavedObject": Object { + "aliasTargetId": undefined, + "outcome": "exactMatch", + }, + "sort": Array [ + Array [ + "order_date", + "desc", + ], + ], + "title": "test1", + } + `); + }); +}); diff --git a/src/plugins/discover/public/saved_searches/get_saved_searches.ts b/src/plugins/discover/public/saved_searches/get_saved_searches.ts new file mode 100644 index 0000000000000..bccf8ad80a8e8 --- /dev/null +++ b/src/plugins/discover/public/saved_searches/get_saved_searches.ts @@ -0,0 +1,78 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectsStart } from '../../../../core/public'; +import type { DataPublicPluginStart } from '../../../data/public'; +import type { SavedSearchAttributes, SavedSearch } from './types'; + +import { SAVED_SEARCH_TYPE } from './constants'; +import { fromSavedSearchAttributes } from './saved_searches_utils'; +import { injectSearchSourceReferences, parseSearchSourceJSON } from '../../../data/public'; + +interface GetSavedSearchDependencies { + search: DataPublicPluginStart['search']; + savedObjectsClient: SavedObjectsStart['client']; +} + +const getEmptySavedSearch = ({ + search, +}: { + search: DataPublicPluginStart['search']; +}): SavedSearch => ({ + searchSource: search.searchSource.createEmpty(), +}); + +const findSavedSearch = async ( + savedSearchId: string, + { search, savedObjectsClient }: GetSavedSearchDependencies +) => { + const so = await savedObjectsClient.resolve( + SAVED_SEARCH_TYPE, + savedSearchId + ); + + if (so) { + const savedSearch = so.saved_object; + + if (!savedSearch.error) { + const parsedSearchSourceJSON = parseSearchSourceJSON( + savedSearch.attributes.kibanaSavedObjectMeta?.searchSourceJSON ?? '{}' + ); + const searchSourceValues = injectSearchSourceReferences( + parsedSearchSourceJSON as Parameters[0], + savedSearch.references + ); + + return fromSavedSearchAttributes( + savedSearchId, + savedSearch.attributes, + await search.searchSource.create(searchSourceValues), + { + outcome: so.outcome, + aliasTargetId: so.alias_target_id, + } + ); + } + } +}; + +/** @public **/ +export const getSavedSearch = async ( + savedSearchId: string | undefined, + dependencies: GetSavedSearchDependencies +) => { + if (savedSearchId) { + const savedSearch = await findSavedSearch(savedSearchId, dependencies); + + if (savedSearch) { + return savedSearch; + } + } + + return getEmptySavedSearch(dependencies); +}; diff --git a/src/plugins/discover/public/saved_searches/index.ts b/src/plugins/discover/public/saved_searches/index.ts index 86564cc5d6eca..30d4decdd5dca 100644 --- a/src/plugins/discover/public/saved_searches/index.ts +++ b/src/plugins/discover/public/saved_searches/index.ts @@ -6,5 +6,18 @@ * Side Public License, v 1. */ -export { createSavedSearchesLoader } from './saved_searches'; -export { SavedSearch, SavedSearchLoader } from './types'; +import { createSavedSearchesLoader } from './legacy/saved_searches'; + +export { getSavedSearch } from './get_saved_searches'; +export { getSavedSearchUrl, getSavedSearchFullPathUrl } from './saved_searches_utils'; +export { useSavedSearchAliasMatchRedirect } from './saved_search_alias_match_redirect'; +export { SavedSearchURLConflictCallout } from './saved_search_url_conflict_callout'; +export { saveSavedSearch, SaveSavedSearchOptions } from './save_saved_searches'; + +export type { SavedSearch } from './types'; +export type { LegacySavedSearch, SavedSearchLoader, SortOrder } from './legacy/types'; + +/** @deprecated __LEGACY object will be removed in v8**/ +export const __LEGACY = { + createSavedSearchesLoader, +}; diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/legacy/_saved_search.ts similarity index 80% rename from src/plugins/discover/public/saved_searches/_saved_search.ts rename to src/plugins/discover/public/saved_searches/legacy/_saved_search.ts index 56533ed20b31e..154f91f5582b3 100644 --- a/src/plugins/discover/public/saved_searches/_saved_search.ts +++ b/src/plugins/discover/public/saved_searches/legacy/_saved_search.ts @@ -6,11 +6,14 @@ * Side Public License, v 1. */ -import { SavedObject, SavedObjectsStart } from '../../../saved_objects/public'; +import type { SavedObject, SavedObjectsStart } from '../../../../saved_objects/public'; +import { SAVED_SEARCH_TYPE } from '../constants'; +import { getSavedSearchFullPathUrl } from '../saved_searches_utils'; +/** @deprecated **/ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { class SavedSearch extends savedObjects.SavedObjectClass { - public static type: string = 'search'; + public static type: string = SAVED_SEARCH_TYPE; public static mapping = { title: 'text', description: 'text', @@ -31,7 +34,7 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { constructor(id: string) { super({ id, - type: 'search', + type: SAVED_SEARCH_TYPE, mapping: { title: 'text', description: 'text', @@ -54,7 +57,7 @@ export function createSavedSearchClass(savedObjects: SavedObjectsStart) { }); this.showInRecentlyAccessed = true; this.id = id; - this.getFullPath = () => `/app/discover#/view/${String(id)}`; + this.getFullPath = () => getSavedSearchFullPathUrl(String(id)); } } diff --git a/src/plugins/discover/public/saved_searches/legacy/index.ts b/src/plugins/discover/public/saved_searches/legacy/index.ts new file mode 100644 index 0000000000000..0bfed6f57b9f5 --- /dev/null +++ b/src/plugins/discover/public/saved_searches/legacy/index.ts @@ -0,0 +1,10 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { createSavedSearchesLoader } from './saved_searches'; +export { LegacySavedSearch, SavedSearchLoader } from './types'; diff --git a/src/plugins/discover/public/saved_searches/saved_searches.ts b/src/plugins/discover/public/saved_searches/legacy/saved_searches.ts similarity index 78% rename from src/plugins/discover/public/saved_searches/saved_searches.ts rename to src/plugins/discover/public/saved_searches/legacy/saved_searches.ts index 0fe693dcd2ebe..58bed080d0249 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches.ts +++ b/src/plugins/discover/public/saved_searches/legacy/saved_searches.ts @@ -6,15 +6,17 @@ * Side Public License, v 1. */ -import { SavedObjectsClientContract } from 'kibana/public'; -import { SavedObjectLoader, SavedObjectsStart } from '../../../saved_objects/public'; +import type { SavedObjectsClientContract } from 'kibana/public'; +import { SavedObjectLoader, SavedObjectsStart } from '../../../../saved_objects/public'; import { createSavedSearchClass } from './_saved_search'; +import { getSavedSearchUrl } from '../saved_searches_utils'; interface Services { savedObjectsClient: SavedObjectsClientContract; savedObjects: SavedObjectsStart; } +/** @deprecated **/ export function createSavedSearchesLoader({ savedObjectsClient, savedObjects }: Services) { const SavedSearchClass = createSavedSearchClass(savedObjects); const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, savedObjectsClient); @@ -25,7 +27,7 @@ export function createSavedSearchesLoader({ savedObjectsClient, savedObjects }: nouns: 'saved searches', }; - savedSearchLoader.urlFor = (id: string) => (id ? `#/view/${encodeURIComponent(id)}` : '#/'); + savedSearchLoader.urlFor = getSavedSearchUrl; return savedSearchLoader; } diff --git a/src/plugins/discover/public/saved_searches/legacy/types.ts b/src/plugins/discover/public/saved_searches/legacy/types.ts new file mode 100644 index 0000000000000..e55422ff26a7b --- /dev/null +++ b/src/plugins/discover/public/saved_searches/legacy/types.ts @@ -0,0 +1,34 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ISearchSource } from '../../../../data/public'; +import type { SavedObjectSaveOpts } from '../../../../saved_objects/public'; +import type { DiscoverGridSettings } from '../../application/components/discover_grid/types'; + +export type SortOrder = [string, string]; + +/** @deprecated **/ +export interface LegacySavedSearch { + readonly id: string; + title: string; + searchSource: ISearchSource; + description?: string; + columns: string[]; + sort: SortOrder[]; + grid: DiscoverGridSettings; + destroy: () => void; + save: (saveOptions: SavedObjectSaveOpts) => Promise; + copyOnSave?: boolean; + hideChart?: boolean; +} + +/** @deprecated **/ +export interface SavedSearchLoader { + get: (id: string) => Promise; + urlFor: (id: string) => string; +} diff --git a/src/plugins/discover/public/saved_searches/save_saved_searches.test.ts b/src/plugins/discover/public/saved_searches/save_saved_searches.test.ts new file mode 100644 index 0000000000000..bf995fffa5f71 --- /dev/null +++ b/src/plugins/discover/public/saved_searches/save_saved_searches.test.ts @@ -0,0 +1,117 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectsStart } from '../../../../core/public'; + +import { savedObjectsServiceMock } from '../../../../core/public/mocks'; +import { dataPluginMock } from '../../../data/public/mocks'; + +import { saveSavedSearch } from './save_saved_searches'; +import type { SavedSearch } from './types'; + +describe('saveSavedSearch', () => { + let savedObjectsClient: SavedObjectsStart['client']; + let savedSearch: SavedSearch; + + beforeEach(() => { + savedObjectsClient = savedObjectsServiceMock.createStartContract().client; + const searchSource = dataPluginMock.createStartContract().search.searchSource.createEmpty(); + + savedSearch = { + id: 'id', + title: 'title', + searchSource: { + ...searchSource, + serialize: () => ({ + searchSourceJSON: '{}', + references: [], + }), + }, + sharingSavedObject: { + outcome: 'aliasMatch', + }, + } as SavedSearch; + }); + + describe('onTitleDuplicate', () => { + test('should check for title duplicating', async () => { + savedObjectsClient.find = jest.fn().mockReturnValue({ + savedObjects: [{ get: () => 'title' }], + }); + const onTitleDuplicate = jest.fn(); + + await saveSavedSearch( + savedSearch, + { + onTitleDuplicate, + copyOnSave: true, + }, + savedObjectsClient + ); + + expect(onTitleDuplicate).toHaveBeenCalled(); + }); + + test('should not check for title duplicating for saving existing search', async () => { + savedObjectsClient.find = jest.fn().mockReturnValue({ + savedObjects: [{ get: () => 'title' }], + }); + const onTitleDuplicate = jest.fn(); + + await saveSavedSearch( + savedSearch, + { + onTitleDuplicate, + copyOnSave: false, + }, + savedObjectsClient + ); + + expect(onTitleDuplicate).not.toHaveBeenCalled(); + }); + }); + + test('should call savedObjectsClient.create for saving new search', async () => { + delete savedSearch.id; + + await saveSavedSearch(savedSearch, {}, savedObjectsClient); + + expect(savedObjectsClient.create).toHaveBeenCalledWith( + 'search', + { + columns: [], + description: '', + grid: {}, + hideChart: false, + kibanaSavedObjectMeta: { searchSourceJSON: '{}' }, + sort: [], + title: 'title', + }, + { references: [] } + ); + }); + + test('should call savedObjectsClient.update for saving existing search', async () => { + await saveSavedSearch(savedSearch, {}, savedObjectsClient); + + expect(savedObjectsClient.update).toHaveBeenCalledWith( + 'search', + 'id', + { + columns: [], + description: '', + grid: {}, + hideChart: false, + kibanaSavedObjectMeta: { searchSourceJSON: '{}' }, + sort: [], + title: 'title', + }, + { references: [] } + ); + }); +}); diff --git a/src/plugins/discover/public/saved_searches/save_saved_searches.ts b/src/plugins/discover/public/saved_searches/save_saved_searches.ts new file mode 100644 index 0000000000000..34cc76bf7b0d6 --- /dev/null +++ b/src/plugins/discover/public/saved_searches/save_saved_searches.ts @@ -0,0 +1,78 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { SavedObjectsStart } from 'kibana/public'; +import type { SavedSearch, SavedSearchAttributes } from './types'; + +import { SAVED_SEARCH_TYPE } from './constants'; +import { toSavedSearchAttributes } from './saved_searches_utils'; + +export interface SaveSavedSearchOptions { + onTitleDuplicate?: () => void; + copyOnSave?: boolean; +} + +const hasDuplicatedTitle = async ( + title: string, + savedObjectsClient: SavedObjectsStart['client'] +): Promise => { + if (!title) { + return; + } + + const response = await savedObjectsClient.find({ + type: SAVED_SEARCH_TYPE, + perPage: 10, + search: `"${title}"`, + searchFields: ['title'], + fields: ['title'], + }); + + return response.savedObjects.some( + (obj) => obj.get('title').toLowerCase() === title.toLowerCase() + ); +}; + +/** @internal **/ +export const saveSavedSearch = async ( + savedSearch: SavedSearch, + options: SaveSavedSearchOptions, + savedObjectsClient: SavedObjectsStart['client'] +): Promise => { + const isNew = options.copyOnSave || !savedSearch.id; + + if (savedSearch.title) { + if ( + isNew && + options.onTitleDuplicate && + (await hasDuplicatedTitle(savedSearch.title, savedObjectsClient)) + ) { + options.onTitleDuplicate(); + return; + } + } + + const { searchSourceJSON, references } = savedSearch.searchSource.serialize(); + const resp = isNew + ? await savedObjectsClient.create( + SAVED_SEARCH_TYPE, + toSavedSearchAttributes(savedSearch, searchSourceJSON), + { + references, + } + ) + : await savedObjectsClient.update( + SAVED_SEARCH_TYPE, + savedSearch.id!, + toSavedSearchAttributes(savedSearch, searchSourceJSON), + { + references, + } + ); + + return resp?.id; +}; diff --git a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts new file mode 100644 index 0000000000000..0c0929eae30e8 --- /dev/null +++ b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts @@ -0,0 +1,65 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { useSavedSearchAliasMatchRedirect } from './saved_search_alias_match_redirect'; +import type { SavedSearch } from './types'; + +import { spacesPluginMock } from '../../../../../x-pack/plugins/spaces/public/mocks'; + +describe('useSavedSearchAliasMatchRedirect', () => { + let spaces: ReturnType; + + beforeEach(() => { + spaces = spacesPluginMock.createStartContract(); + }); + + test("should render URLConflictCallout in case of id's conflicts", () => { + const savedSearch = { + id: 'id', + sharingSavedObject: { + outcome: 'aliasMatch', + aliasTargetId: 'aliasTargetId', + }, + } as SavedSearch; + + renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch })); + + expect(spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith( + '/app/discover#/view/aliasTargetId', + ' search' + ); + }); + + test('should not render URLConflictCallout if outcome !== aliasMatch', () => { + const savedSearch = { + id: 'id', + sharingSavedObject: { + outcome: 'exactMatch', + }, + } as SavedSearch; + + renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch })); + + expect(spaces.ui.redirectLegacyUrl).not.toHaveBeenCalled(); + }); + + test('should not render URLConflictCallout if aliasTargetId is not defined', () => { + const savedSearch = { + id: 'id', + sharingSavedObject: { + outcome: 'aliasMatch', + }, + } as SavedSearch; + + renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch })); + + expect(spaces.ui.redirectLegacyUrl).not.toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts new file mode 100644 index 0000000000000..5683edcd2091b --- /dev/null +++ b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts @@ -0,0 +1,48 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { getSavedSearchFullPathUrl } from './saved_searches_utils'; +import { SAVED_SEARCH_TYPE } from './constants'; + +import type { SavedSearch } from './types'; +import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; + +interface SavedSearchAliasMatchRedirectProps { + savedSearch?: SavedSearch; + spaces?: SpacesApi; +} + +export const useSavedSearchAliasMatchRedirect = ({ + savedSearch, + spaces, +}: SavedSearchAliasMatchRedirectProps) => { + useEffect(() => { + async function aliasMatchRedirect() { + if (savedSearch) { + const { aliasTargetId, outcome } = savedSearch.sharingSavedObject ?? {}; + + if (spaces && aliasTargetId && outcome === 'aliasMatch') { + await spaces.ui.redirectLegacyUrl( + getSavedSearchFullPathUrl(aliasTargetId), + i18n.translate('discover.savedSearchAliasMatchRedirect.objectNoun', { + defaultMessage: '{savedSearch} {type}', + values: { + savedSearch: savedSearch.title, + type: SAVED_SEARCH_TYPE, + }, + }) + ); + } + } + } + + aliasMatchRedirect(); + }, [savedSearch, spaces]); +}; diff --git a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx new file mode 100644 index 0000000000000..e50c03765ae29 --- /dev/null +++ b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx @@ -0,0 +1,52 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { SavedSearchURLConflictCallout } from './saved_search_url_conflict_callout'; +import type { SavedSearch } from './types'; + +import { spacesPluginMock } from '../../../../../x-pack/plugins/spaces/public/mocks'; + +describe('SavedSearchURLConflictCallout', () => { + let spaces: ReturnType; + + beforeEach(() => { + spaces = spacesPluginMock.createStartContract(); + spaces.ui.components.getLegacyUrlConflict = jest.fn().mockReturnValue('callout'); + }); + + test("should render URLConflictCallout in case of id's conflicts", () => { + const savedSearch = { + id: 'id', + sharingSavedObject: { + outcome: 'conflict', + aliasTargetId: 'aliasTargetId', + }, + } as SavedSearch; + + const component = mountWithIntl( + + ); + + expect(component.children()).toMatchInlineSnapshot(`"callout"`); + }); + + test('should not render URLConflictCallout in case of no conflicts', () => { + const savedSearch = { + id: 'id', + sharingSavedObject: {}, + } as SavedSearch; + + const component = mountWithIntl( + + ); + + expect(component.children()).toMatchInlineSnapshot(`null`); + }); +}); diff --git a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts new file mode 100644 index 0000000000000..1ea56de62d2e1 --- /dev/null +++ b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts @@ -0,0 +1,45 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { getSavedSearchUrl } from './saved_searches_utils'; +import { SAVED_SEARCH_TYPE } from './constants'; + +import type { SavedSearch } from './types'; +import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; + +interface SavedSearchURLConflictCalloutProps { + savedSearch?: SavedSearch; + spaces?: SpacesApi; +} + +export const SavedSearchURLConflictCallout = ({ + savedSearch, + spaces, +}: SavedSearchURLConflictCalloutProps) => { + if (spaces && savedSearch?.id && savedSearch?.sharingSavedObject?.outcome === 'conflict') { + const otherObjectId = savedSearch.sharingSavedObject?.aliasTargetId; + + if (otherObjectId) { + return spaces.ui.components.getLegacyUrlConflict({ + objectNoun: i18n.translate('discover.savedSearchURLConflictCallout.objectNoun', { + defaultMessage: '{savedSearch} {type}', + values: { + savedSearch: savedSearch.title, + type: SAVED_SEARCH_TYPE, + }, + }), + currentObjectId: savedSearch.id, + otherObjectPath: getSavedSearchUrl(otherObjectId), + otherObjectId, + }); + } + } + + return null; +}; diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts new file mode 100644 index 0000000000000..1e88e488ef2d6 --- /dev/null +++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts @@ -0,0 +1,117 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + getSavedSearchUrl, + getSavedSearchFullPathUrl, + fromSavedSearchAttributes, + toSavedSearchAttributes, +} from './saved_searches_utils'; + +import { createSearchSourceMock } from '../../../data/public/mocks'; + +import type { SavedSearchAttributes, SavedSearch } from './types'; + +describe('saved_searches_utils', () => { + describe('getSavedSearchUrl', () => { + test('should return valid saved search url', () => { + expect(getSavedSearchUrl()).toBe('#/'); + expect(getSavedSearchUrl('id')).toBe('#/view/id'); + }); + }); + + describe('getSavedSearchFullPathUrl', () => { + test('should return valid full path url', () => { + expect(getSavedSearchFullPathUrl()).toBe('/app/discover#/'); + expect(getSavedSearchFullPathUrl('id')).toBe('/app/discover#/view/id'); + }); + }); + + describe('fromSavedSearchAttributes', () => { + test('should convert attributes into SavedSearch', () => { + const attributes: SavedSearchAttributes = { + kibanaSavedObjectMeta: { searchSourceJSON: '{}' }, + title: 'saved search', + sort: [], + columns: ['a', 'b'], + description: 'foo', + grid: {}, + hideChart: true, + }; + + expect(fromSavedSearchAttributes('id', attributes, createSearchSourceMock(), {})) + .toMatchInlineSnapshot(` + Object { + "columns": Array [ + "a", + "b", + ], + "description": "foo", + "grid": Object {}, + "hideChart": true, + "id": "id", + "searchSource": SearchSource { + "dependencies": Object { + "getConfig": [MockFunction], + "onResponse": [MockFunction], + "search": [MockFunction], + }, + "fields": Object {}, + "getFieldName": [Function], + "history": Array [], + "id": "data_source1", + "inheritOptions": Object {}, + "parent": undefined, + "requestStartHandlers": Array [], + "searchStrategyId": undefined, + }, + "sharingSavedObject": Object {}, + "sort": Array [], + "title": "saved search", + } + `); + }); + }); + + describe('toSavedSearchAttributes', () => { + test('should serialize SavedSearch attributes', () => { + const savedSearch: SavedSearch = { + id: 'id', + searchSource: createSearchSourceMock(), + title: 'title', + sort: [['a', 'asc']], + columns: ['c', 'd'], + description: 'description', + grid: {}, + hideChart: true, + }; + + expect(toSavedSearchAttributes(savedSearch, '{}')).toMatchInlineSnapshot(` + Object { + "columns": Array [ + "c", + "d", + ], + "description": "description", + "grid": Object {}, + "hideChart": true, + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{}", + }, + "sort": Array [ + Array [ + "a", + "asc", + ], + ], + "title": "title", + } + `); + }); + }); +}); diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts new file mode 100644 index 0000000000000..a24413f79d6b0 --- /dev/null +++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts @@ -0,0 +1,43 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedSearchAttributes, SavedSearch } from './types'; + +export const getSavedSearchUrl = (id?: string) => (id ? `#/view/${encodeURIComponent(id)}` : '#/'); + +export const getSavedSearchFullPathUrl = (id?: string) => `/app/discover${getSavedSearchUrl(id)}`; + +export const fromSavedSearchAttributes = ( + id: string, + attributes: SavedSearchAttributes, + searchSource: SavedSearch['searchSource'], + sharingSavedObject: SavedSearch['sharingSavedObject'] +): SavedSearch => ({ + id, + searchSource, + sharingSavedObject, + title: attributes.title, + sort: attributes.sort, + columns: attributes.columns, + description: attributes.description, + grid: attributes.grid, + hideChart: attributes.hideChart, +}); + +export const toSavedSearchAttributes = ( + savedSearch: SavedSearch, + searchSourceJSON: string +): SavedSearchAttributes => ({ + kibanaSavedObjectMeta: { searchSourceJSON }, + title: savedSearch.title ?? '', + sort: savedSearch.sort ?? [], + columns: savedSearch.columns ?? [], + description: savedSearch.description ?? '', + grid: savedSearch.grid ?? {}, + hideChart: savedSearch.hideChart ?? false, +}); diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index b1c7b48d696b3..860d9fed02ac4 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -6,26 +6,38 @@ * Side Public License, v 1. */ -import { SearchSource } from '../../../data/public'; -import { SavedObjectSaveOpts } from '../../../saved_objects/public'; -import { DiscoverGridSettings } from '../application/components/discover_grid/types'; +import type { ISearchSource } from '../../../data/public'; +import { DiscoverGridSettingsColumn } from '../application/components/discover_grid/types'; -export type SortOrder = [string, string]; -export interface SavedSearch { - readonly id: string; +/** @internal **/ +export interface SavedSearchAttributes { title: string; - searchSource: SearchSource; - description?: string; + sort: Array<[string, string]>; columns: string[]; - sort: SortOrder[]; - grid: DiscoverGridSettings; - destroy: () => void; - save: (saveOptions: SavedObjectSaveOpts) => Promise; - lastSavedTitle?: string; - copyOnSave?: boolean; - hideChart?: boolean; + description: string; + grid: { + columns?: Record; + }; + hideChart: boolean; + kibanaSavedObjectMeta: { + searchSourceJSON: string; + }; } -export interface SavedSearchLoader { - get: (id: string) => Promise; - urlFor: (id: string) => string; + +/** @public **/ +export interface SavedSearch { + searchSource: ISearchSource; + id?: string; + title?: string; + sort?: Array<[string, string]>; + columns?: string[]; + description?: string; + grid?: { + columns?: Record; + }; + hideChart?: boolean; + sharingSavedObject?: { + outcome?: 'aliasMatch' | 'exactMatch' | 'conflict'; + aliasTargetId?: string; + }; } diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index b3f1ad5d0bc1e..7a6736b30d9f3 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../kibana_react/tsconfig.json" }, { "path": "../kibana_legacy/tsconfig.json" }, { "path": "../index_pattern_field_editor/tsconfig.json"}, - { "path": "../field_formats/tsconfig.json" } + { "path": "../field_formats/tsconfig.json" }, + { "path": "../../../x-pack/plugins/spaces/tsconfig.json" } ] } diff --git a/src/plugins/vis_default_editor/kibana.json b/src/plugins/vis_default_editor/kibana.json index e85c5713eb82c..efed1eab1e494 100644 --- a/src/plugins/vis_default_editor/kibana.json +++ b/src/plugins/vis_default_editor/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "ui": true, "optionalPlugins": ["visualize"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "fieldFormats"], + "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "fieldFormats", "discover"], "owner": { "name": "Vis Editors", "githubTeam": "kibana-vis-editors" diff --git a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx index dab982e5a8070..f1eebbbdf2116 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx @@ -26,7 +26,7 @@ import { } from 'src/plugins/visualizations/public'; import type { Schema } from 'src/plugins/visualizations/public'; import { TimeRange } from 'src/plugins/data/public'; -import { SavedObject } from 'src/plugins/saved_objects/public'; +import { SavedSearch } from 'src/plugins/discover/public'; import { DefaultEditorNavBar } from './navbar'; import { DefaultEditorControls } from './controls'; import { setStateParamValue, useEditorReducer, useEditorFormState, discardChanges } from './state'; @@ -42,7 +42,7 @@ interface DefaultEditorSideBarProps { vis: Vis; isLinkedSearch: boolean; eventEmitter: EventEmitter; - savedSearch?: SavedObject; + savedSearch?: SavedSearch; timeRange: TimeRange; } diff --git a/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx index cab27d53b827d..2740f4ff50b4e 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar_title.tsx @@ -25,18 +25,18 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Vis } from 'src/plugins/visualizations/public'; -import { SavedObject } from 'src/plugins/saved_objects/public'; +import { SavedSearch, getSavedSearchUrl } from '../../../../discover/public'; import { ApplicationStart } from '../../../../../core/public'; import { useKibana } from '../../../../kibana_react/public'; interface LinkedSearchProps { - savedSearch: SavedObject; + savedSearch: SavedSearch; eventEmitter: EventEmitter; } interface SidebarTitleProps { isLinkedSearch: boolean; - savedSearch?: SavedObject; + savedSearch?: SavedSearch; vis: Vis; eventEmitter: EventEmitter; } @@ -55,7 +55,7 @@ export function LinkedSearch({ savedSearch, eventEmitter }: LinkedSearchProps) { }, [eventEmitter]); const onClickViewInDiscover = useCallback(() => { application.navigateToApp('discover', { - path: `#/view/${savedSearch.id}`, + path: getSavedSearchUrl(savedSearch.id), }); }, [application, savedSearch.id]); diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index ee3e914aa4bc6..d8002a60256d7 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -21,7 +21,6 @@ import { setAggs, setChrome, setOverlays, - setSavedSearchLoader, setEmbeddable, setDocLinks, } from './services'; @@ -45,8 +44,6 @@ import { convertToSerializedVis, } from './saved_visualizations/_saved_vis'; -import { createSavedSearchesLoader } from '../../discover/public'; - import type { PluginInitializerContext, CoreSetup, @@ -173,11 +170,7 @@ export class VisualizationsPlugin visualizationTypes: types, }); setSavedVisualizationsLoader(savedVisualizationsLoader); - const savedSearchLoader = createSavedSearchesLoader({ - savedObjectsClient: core.savedObjects.client, - savedObjects, - }); - setSavedSearchLoader(savedSearchLoader); + return { ...types, showNewVisModal, diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts index fb6c99ac8ef02..90dd774c4acac 100644 --- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts @@ -17,7 +17,6 @@ import type { SavedObjectsStart, SavedObject } from '../../../../plugins/saved_o // @ts-ignore import { updateOldState } from '../legacy/vis_update_state'; import { extractReferences, injectReferences } from './saved_visualization_references'; -import { createSavedSearchesLoader } from '../../../discover/public'; import type { SavedObjectsClientContract } from '../../../../core/public'; import type { IndexPatternsContract } from '../../../../plugins/data/public'; import type { ISavedVis, SerializedVis } from '../types'; @@ -66,8 +65,6 @@ export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => { }; export function createSavedVisClass(services: SavedVisServices) { - const savedSearch = createSavedSearchesLoader(services); - class SavedVis extends services.savedObjects.SavedObjectClass { public static type: string = 'visualization'; public static mapping: Record = { @@ -108,9 +105,6 @@ export function createSavedVisClass(services: SavedVisServices) { if (savedVis.searchSourceFields?.index) { await services.indexPatterns.get(savedVis.searchSourceFields.index as any); } - if (savedVis.savedSearchId) { - await savedSearch.get(savedVis.savedSearchId); - } return savedVis as any as SavedObject; }, }); diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index f1ab9077cd207..f6b8145b3e352 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -23,7 +23,6 @@ import { UsageCollectionSetup } from '../../../plugins/usage_collection/public'; import { ExpressionsStart } from '../../../plugins/expressions/public'; import { UiActionsStart } from '../../../plugins/ui_actions/public'; import { SavedVisualizationsLoader } from './saved_visualizations'; -import { SavedObjectLoader } from '../../saved_objects/public'; import { EmbeddableStart } from '../../embeddable/public'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); @@ -63,6 +62,3 @@ export const [getAggs, setAggs] = export const [getOverlays, setOverlays] = createGetterSetter('Overlays'); export const [getChrome, setChrome] = createGetterSetter('Chrome'); - -export const [getSavedSearchLoader, setSavedSearchLoader] = - createGetterSetter('savedSearchLoader'); diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index dfab4ecfc3cd8..b879b683bf7b0 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -21,17 +21,19 @@ import { Assign } from '@kbn/utility-types'; import { i18n } from '@kbn/i18n'; import { PersistedState } from './persisted_state'; -import { getTypes, getAggs, getSearch, getSavedSearchLoader } from './services'; +import { getTypes, getAggs, getSearch, getSavedObjects } from './services'; import { IAggConfigs, IndexPattern, ISearchSource, AggConfigSerialized, SearchSourceFields, -} from '../../../plugins/data/public'; +} from '../../data/public'; import { BaseVisType } from './vis_types'; import { VisParams } from '../common/types'; +import { getSavedSearch } from '../../discover/public'; + export interface SerializedVisData { expression?: string; aggs: AggConfigSerialized[]; @@ -58,14 +60,17 @@ export interface VisData { } const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: string) => { - const searchSource = inputSearchSource.createCopy(); if (savedSearchId) { - const savedSearch = await getSavedSearchLoader().get(savedSearchId); + const savedSearchSearch = await getSavedSearch(savedSearchId, { + search: getSearch(), + savedObjectsClient: getSavedObjects().client, + }); - searchSource.setParent(savedSearch.searchSource); + if (savedSearchSearch?.searchSource) { + return savedSearchSearch?.searchSource; + } } - searchSource.setField('size', 0); - return searchSource; + return inputSearchSource; }; type PartialVisState = Assign }>; diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 7e9f69163f5a6..be49895a94114 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -38,13 +38,14 @@ import type { import type { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; import type { Query, Filter, DataPublicPluginStart, TimeRange } from 'src/plugins/data/public'; import type { SharePluginStart } from 'src/plugins/share/public'; -import type { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public'; +import type { SavedObjectsStart } from 'src/plugins/saved_objects/public'; import type { EmbeddableStart, EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; import type { UrlForwardingStart } from 'src/plugins/url_forwarding/public'; import type { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public'; import type { DashboardStart } from '../../../dashboard/public'; import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; import type { UsageCollectionStart } from '../../../usage_collection/public'; +import type { SavedSearch } from '../../../discover/public'; import { PureVisState } from '../../common/types'; @@ -107,20 +108,15 @@ export interface VisualizeServices extends CoreStart { getKibanaVersion: () => string; } -export interface SavedVisInstance { +export interface VisInstance { vis: Vis; savedVis: VisSavedObject; - savedSearch?: SavedObject; - embeddableHandler: VisualizeEmbeddableContract; -} - -export interface ByValueVisInstance { - vis: Vis; - savedVis: VisSavedObject; - savedSearch?: SavedObject; + savedSearch?: SavedSearch; embeddableHandler: VisualizeEmbeddableContract; } +export type SavedVisInstance = VisInstance; +export type ByValueVisInstance = VisInstance; export type VisualizeEditorVisInstance = SavedVisInstance | ByValueVisInstance; export type VisEditorConstructor = new ( @@ -141,7 +137,7 @@ export interface EditorRenderProps { filters: Filter[]; timeRange: TimeRange; query?: Query; - savedSearch?: SavedObject; + savedSearch?: SavedSearch; uiState: PersistedState; /** * Flag to determine if visualiztion is linked to the saved search diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts index 83462ba9ae7b9..337f9d5c10000 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { createSavedSearchesLoader } from '../../../../discover/public'; +import { getSavedSearch } from '../../../../discover/public'; import type { VisualizeInput, VisSavedObject, @@ -21,13 +21,12 @@ import { createVisualizeServicesMock } from './mocks'; import { VisualizeServices } from '../types'; import { BehaviorSubject } from 'rxjs'; -const mockSavedSearchObj = {}; -const mockGetSavedSearch = jest.fn(() => mockSavedSearchObj); - jest.mock('../../../../discover/public', () => ({ - createSavedSearchesLoader: jest.fn(() => ({ - get: mockGetSavedSearch, - })), + getSavedSearch: jest.fn().mockResolvedValue({ + id: 'savedSearch', + title: 'savedSearchTitle', + searchSource: {}, + }), })); describe('getVisualizationInstance', () => { @@ -108,9 +107,14 @@ describe('getVisualizationInstance', () => { visMock.data.savedSearchId = 'saved_search_id'; const { savedSearch } = await getVisualizationInstance(mockServices, 'saved_vis_id'); - expect(createSavedSearchesLoader).toHaveBeenCalled(); - expect(mockGetSavedSearch).toHaveBeenCalledWith(visMock.data.savedSearchId); - expect(savedSearch).toBe(mockSavedSearchObj); + expect(getSavedSearch).toHaveBeenCalled(); + expect(savedSearch).toMatchInlineSnapshot(` + Object { + "id": "savedSearch", + "searchSource": Object {}, + "title": "savedSearchTitle", + } + `); }); test('should subscribe on embeddable handler updates and send toasts on errors', async () => { diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index 88797ce264e25..9ac86469b0da3 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -14,10 +14,9 @@ import { VisualizeInput, } from 'src/plugins/visualizations/public'; import { SearchSourceFields } from 'src/plugins/data/public'; -import { SavedObject } from 'src/plugins/saved_objects/public'; import { cloneDeep } from 'lodash'; import { ExpressionValueError } from 'src/plugins/expressions/public'; -import { createSavedSearchesLoader } from '../../../../discover/public'; +import { getSavedSearch, SavedSearch } from '../../../../discover/public'; import { SavedFieldNotFound, SavedFieldTypeInvalidForAgg } from '../../../../kibana_utils/common'; import { VisualizeServices } from '../types'; @@ -33,8 +32,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( vis: Vis, visualizeServices: VisualizeServices ) => { - const { data, createVisEmbeddableFromObject, savedObjects, savedObjectsPublic } = - visualizeServices; + const { data, createVisEmbeddableFromObject, savedObjects } = visualizeServices; const embeddableHandler = (await createVisEmbeddableFromObject(vis, { id: '', timeRange: data.query.timefilter.timefilter.getTime(), @@ -50,13 +48,13 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( } }); - let savedSearch: SavedObject | undefined; + let savedSearch: SavedSearch | undefined; if (vis.data.savedSearchId) { - savedSearch = await createSavedSearchesLoader({ + savedSearch = await getSavedSearch(vis.data.savedSearchId, { + search: data.search, savedObjectsClient: savedObjects.client, - savedObjects: savedObjectsPublic, - }).get(vis.data.savedSearchId); + }); } return { savedSearch, embeddableHandler }; diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts index bedf310725ae2..a4fe1835ce5f7 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts @@ -107,7 +107,7 @@ describe('GetCsvReportPanelAction', () => { columns: [], objectType: 'downloadCsv', searchSource: {}, - title: undefined, + title: '', version: '7.15.0', }); }); @@ -144,7 +144,7 @@ describe('GetCsvReportPanelAction', () => { columns: ['column_a', 'column_b'], objectType: 'downloadCsv', searchSource: { testData: 'testDataValue' }, - title: undefined, + title: '', version: '7.15.0', }); }); 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 ef32e64741765..6bf52aa6e3281 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 @@ -122,7 +122,7 @@ export class ReportingCsvPanelAction implements ActionDefinition const immediateJobParams = this.apiClient.getDecoratedJobParams({ searchSource, columns, - title: savedSearch.title, + title: savedSearch.title || '', objectType: 'downloadCsv', // FIXME: added for typescript, but immediate download job does not need objectType }); diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index ae072e6666e4a..e0788d7747e65 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -13,7 +13,7 @@ export const useRequest = jest.fn(() => ({ error: null, data: undefined, })); -export const createSavedSearchesLoader = jest.fn(); +export const getSavedSearch = jest.fn(); // just passing through the reimports export { getMlSharedImports, ES_CLIENT_TOTAL_HITS_RELATION } from '../../../ml/public'; diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts index 3e85f5d4d49a4..db5fd82808a91 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/common.ts @@ -20,12 +20,10 @@ import { isIndexPattern } from '../../../../common/types/index_pattern'; export type SavedSearchQuery = object; type IndexPatternId = string; -type SavedSearchId = string; let indexPatternCache: Array>> = []; let fullIndexPatterns; let currentIndexPattern = null; -let currentSavedSearch = null; export let refreshIndexPatterns: () => Promise; @@ -76,11 +74,6 @@ export function loadCurrentIndexPattern( return currentIndexPattern; } -export function loadCurrentSavedSearch(savedSearches: any, savedSearchId: SavedSearchId) { - currentSavedSearch = savedSearches.get(savedSearchId); - return currentSavedSearch; -} - export interface SearchItems { indexPattern: IndexPattern; savedSearch: any; diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index 44e3cc347824d..f9bad6e8261c0 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { isIndexPattern } from '../../../../common/types/index_pattern'; -import { createSavedSearchesLoader } from '../../../shared_imports'; +import { getSavedSearch } from '../../../shared_imports'; import { useAppDependencies } from '../../app_dependencies'; @@ -20,7 +20,6 @@ import { getIndexPatternIdByTitle, loadCurrentIndexPattern, loadIndexPatterns, - loadCurrentSavedSearch, SearchItems, } from './common'; @@ -32,10 +31,6 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { const indexPatterns = appDeps.data.indexPatterns; const uiSettings = appDeps.uiSettings; const savedObjectsClient = appDeps.savedObjects.client; - const savedSearches = createSavedSearchesLoader({ - savedObjectsClient, - savedObjects: appDeps.savedObjectsPlugin, - }); const [searchItems, setSearchItems] = useState(undefined); @@ -52,7 +47,14 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { } try { - fetchedSavedSearch = await loadCurrentSavedSearch(savedSearches, id); + const savedSearch = await getSavedSearch(id, { + search: appDeps.data.search, + savedObjectsClient: appDeps.savedObjects.client, + }); + + if (savedSearch.id) { + fetchedSavedSearch = savedSearch; + } } catch (e) { // Just let fetchedSavedSearch stay undefined in case it doesn't exist. } diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index edd27fd43c2af..b3662466a53f5 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -5,9 +5,9 @@ * 2.0. */ -export { createSavedSearchesLoader } from '../../../../src/plugins/discover/public'; export { XJsonMode } from '@kbn/ace'; export { UseRequestConfig, useRequest } from '../../../../src/plugins/es_ui_shared/public'; +export { getSavedSearch } from '../../../../src/plugins/discover/public'; export { getMlSharedImports, From 38258c53519cde23d6e19be930c2aa29594bbb89 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Mon, 4 Oct 2021 22:08:19 +0300 Subject: [PATCH 02/18] Fix PR comments --- .../apps/main/services/use_discover_state.ts | 30 +++++++++---------- .../saved_search_alias_match_redirect.test.ts | 5 +--- .../saved_search_alias_match_redirect.ts | 4 +-- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts index bdf40bebed193..287e965432aef 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -147,22 +147,20 @@ export function useDiscoverState({ */ const resetSavedSearch = useCallback( async (id?: string) => { - if (id) { - const newSavedSearch = await getSavedSearch(id, { - search: services.data.search, - savedObjectsClient: services.core.savedObjects.client, - }); - - const newIndexPattern = newSavedSearch.searchSource.getField('index') || indexPattern; - newSavedSearch.searchSource.setField('index', newIndexPattern); - const newAppState = getStateDefaults({ - config, - data, - savedSearch: newSavedSearch, - }); - await stateContainer.replaceUrlAppState(newAppState); - setState(newAppState); - } + const newSavedSearch = await getSavedSearch(id, { + search: services.data.search, + savedObjectsClient: services.core.savedObjects.client, + }); + + const newIndexPattern = newSavedSearch.searchSource.getField('index') || indexPattern; + newSavedSearch.searchSource.setField('index', newIndexPattern); + const newAppState = getStateDefaults({ + config, + data, + savedSearch: newSavedSearch, + }); + await stateContainer.replaceUrlAppState(newAppState); + setState(newAppState); }, [indexPattern, services, config, data, stateContainer] ); diff --git a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts index 0c0929eae30e8..e02c01ca1f572 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts @@ -31,10 +31,7 @@ describe('useSavedSearchAliasMatchRedirect', () => { renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch })); - expect(spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith( - '/app/discover#/view/aliasTargetId', - ' search' - ); + expect(spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith('#/view/aliasTargetId', ' search'); }); test('should not render URLConflictCallout if outcome !== aliasMatch', () => { diff --git a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts index 5683edcd2091b..a0448265725bb 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts @@ -8,7 +8,7 @@ import { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { getSavedSearchFullPathUrl } from './saved_searches_utils'; +import { getSavedSearchUrl } from './saved_searches_utils'; import { SAVED_SEARCH_TYPE } from './constants'; import type { SavedSearch } from './types'; @@ -30,7 +30,7 @@ export const useSavedSearchAliasMatchRedirect = ({ if (spaces && aliasTargetId && outcome === 'aliasMatch') { await spaces.ui.redirectLegacyUrl( - getSavedSearchFullPathUrl(aliasTargetId), + getSavedSearchUrl(aliasTargetId), i18n.translate('discover.savedSearchAliasMatchRedirect.objectNoun', { defaultMessage: '{savedSearch} {type}', values: { From a8df73168a8a0b0efba01672cce2ed93b2e6c58a Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 5 Oct 2021 11:30:34 +0300 Subject: [PATCH 03/18] fix test names --- .../saved_search_alias_match_redirect.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts index e02c01ca1f572..c5e706a76c1c0 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts @@ -20,7 +20,7 @@ describe('useSavedSearchAliasMatchRedirect', () => { spaces = spacesPluginMock.createStartContract(); }); - test("should render URLConflictCallout in case of id's conflicts", () => { + test('should redirect in case of aliasMatch', () => { const savedSearch = { id: 'id', sharingSavedObject: { @@ -34,7 +34,7 @@ describe('useSavedSearchAliasMatchRedirect', () => { expect(spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith('#/view/aliasTargetId', ' search'); }); - test('should not render URLConflictCallout if outcome !== aliasMatch', () => { + test('should not redirect if outcome !== aliasMatch', () => { const savedSearch = { id: 'id', sharingSavedObject: { @@ -47,7 +47,7 @@ describe('useSavedSearchAliasMatchRedirect', () => { expect(spaces.ui.redirectLegacyUrl).not.toHaveBeenCalled(); }); - test('should not render URLConflictCallout if aliasTargetId is not defined', () => { + test('should not redirect if aliasTargetId is not defined', () => { const savedSearch = { id: 'id', sharingSavedObject: { From 7f3f67652657edfea056d7377dd9c24eb5a6f6ff Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 5 Oct 2021 12:30:25 +0300 Subject: [PATCH 04/18] fix ts error --- .../application/apps/main/utils/get_fetch_observable.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts index aac6196e64f6f..f09251f60d179 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts @@ -12,7 +12,7 @@ import { FetchStatus } from '../../../types'; import type { AutoRefreshDoneFn, DataPublicPluginStart, - SearchSource, + ISearchSource, } from '../../../../../../data/public'; import { DataMain$, DataRefetch$ } from '../services/use_saved_search'; import { DiscoverSearchSessionManager } from '../services/discover_search_session'; @@ -32,7 +32,7 @@ export function getFetch$({ main$: DataMain$; refetch$: DataRefetch$; searchSessionManager: DiscoverSearchSessionManager; - searchSource: SearchSource; + searchSource: ISearchSource; }) { const { timefilter } = data.query.timefilter; const { filterManager } = data.query; From dc3ee7baab8ed531d8251524a8fdbc3d3395f417 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 5 Oct 2021 13:19:09 +0300 Subject: [PATCH 05/18] add handling of missed 'so' --- .../saved_searches/get_saved_searches.test.ts | 30 +++++++++++ .../saved_searches/get_saved_searches.ts | 54 +++++++++---------- .../use_search_items/use_search_items.ts | 6 +-- 3 files changed, 56 insertions(+), 34 deletions(-) diff --git a/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts b/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts index d0c53f3556f29..6306938ab4e38 100644 --- a/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts +++ b/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts @@ -29,6 +29,36 @@ describe('getSavedSearch', () => { expect(savedSearch).toHaveProperty('searchSource'); }); + test('should throw an error if so not found', async () => { + let errorMessage = 'No error thrown.'; + savedObjectsClient.resolve = jest.fn().mockReturnValue({ + saved_object: { + attributes: {}, + error: { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [search/ccf1af80-2297-11ec-86e0-1155ffb9c7a7] not found', + }, + id: 'ccf1af80-2297-11ec-86e0-1155ffb9c7a7', + type: 'search', + references: [], + }, + }); + + try { + await getSavedSearch('ccf1af80-2297-11ec-86e0-1155ffb9c7a7', { + savedObjectsClient, + search, + }); + } catch (error) { + errorMessage = error.message; + } + + expect(errorMessage).toBe( + 'Could not locate that search (id: ccf1af80-2297-11ec-86e0-1155ffb9c7a7)' + ); + }); + test('should find saved search', async () => { savedObjectsClient.resolve = jest.fn().mockReturnValue({ saved_object: { diff --git a/src/plugins/discover/public/saved_searches/get_saved_searches.ts b/src/plugins/discover/public/saved_searches/get_saved_searches.ts index bccf8ad80a8e8..5d37d7dcfc510 100644 --- a/src/plugins/discover/public/saved_searches/get_saved_searches.ts +++ b/src/plugins/discover/public/saved_searches/get_saved_searches.ts @@ -13,6 +13,7 @@ import type { SavedSearchAttributes, SavedSearch } from './types'; import { SAVED_SEARCH_TYPE } from './constants'; import { fromSavedSearchAttributes } from './saved_searches_utils'; import { injectSearchSourceReferences, parseSearchSourceJSON } from '../../../data/public'; +import { SavedObjectNotFound } from '../../../kibana_utils/public'; interface GetSavedSearchDependencies { search: DataPublicPluginStart['search']; @@ -36,29 +37,30 @@ const findSavedSearch = async ( savedSearchId ); - if (so) { - const savedSearch = so.saved_object; + if (!so.saved_object || so.saved_object.error) { + throw new SavedObjectNotFound(SAVED_SEARCH_TYPE, savedSearchId); + } + + const savedSearch = so.saved_object; + + const parsedSearchSourceJSON = parseSearchSourceJSON( + savedSearch.attributes.kibanaSavedObjectMeta?.searchSourceJSON ?? '{}' + ); - if (!savedSearch.error) { - const parsedSearchSourceJSON = parseSearchSourceJSON( - savedSearch.attributes.kibanaSavedObjectMeta?.searchSourceJSON ?? '{}' - ); - const searchSourceValues = injectSearchSourceReferences( - parsedSearchSourceJSON as Parameters[0], - savedSearch.references - ); + const searchSourceValues = injectSearchSourceReferences( + parsedSearchSourceJSON as Parameters[0], + savedSearch.references + ); - return fromSavedSearchAttributes( - savedSearchId, - savedSearch.attributes, - await search.searchSource.create(searchSourceValues), - { - outcome: so.outcome, - aliasTargetId: so.alias_target_id, - } - ); + return fromSavedSearchAttributes( + savedSearchId, + savedSearch.attributes, + await search.searchSource.create(searchSourceValues), + { + outcome: so.outcome, + aliasTargetId: so.alias_target_id, } - } + ); }; /** @public **/ @@ -66,13 +68,7 @@ export const getSavedSearch = async ( savedSearchId: string | undefined, dependencies: GetSavedSearchDependencies ) => { - if (savedSearchId) { - const savedSearch = await findSavedSearch(savedSearchId, dependencies); - - if (savedSearch) { - return savedSearch; - } - } - - return getEmptySavedSearch(dependencies); + return savedSearchId + ? findSavedSearch(savedSearchId, dependencies) + : getEmptySavedSearch(dependencies); }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index f9bad6e8261c0..28cff69a6ef90 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -47,14 +47,10 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { } try { - const savedSearch = await getSavedSearch(id, { + fetchedSavedSearch = await getSavedSearch(id, { search: appDeps.data.search, savedObjectsClient: appDeps.savedObjects.client, }); - - if (savedSearch.id) { - fetchedSavedSearch = savedSearch; - } } catch (e) { // Just let fetchedSavedSearch stay undefined in case it doesn't exist. } From 232109db18d8045dc43ad8acfec3d1fd6baae6db Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 5 Oct 2021 14:43:22 +0300 Subject: [PATCH 06/18] add Embeddable error --- .../embeddable/search_embeddable_factory.ts | 23 ++++++++++++++++++- .../discover/public/saved_searches/index.ts | 8 ++++++- .../saved_search_url_conflict_callout.ts | 4 ++-- .../saved_searches/saved_searches_utils.ts | 3 +++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts index 645e3c5c67357..efb24384a8ae9 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts @@ -20,7 +20,12 @@ import { TimeRange } from '../../../../data/public'; import { SearchInput, SearchOutput } from './types'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; import { SavedSearchEmbeddable } from './saved_search_embeddable'; -import { getSavedSearch, getSavedSearchUrl } from '../../saved_searches'; +import { + getSavedSearch, + getSavedSearchUrl, + savedSearchHasUrlConflict, + SAVED_SEARCH_TYPE, +} from '../../saved_searches'; interface StartServices { executeTriggerActions: UiActionsStart['executeTriggerActions']; @@ -70,6 +75,22 @@ export class SearchEmbeddableFactory savedObjectsClient: services.core.savedObjects.client, }); + if (savedSearchHasUrlConflict(savedSearch)) { + throw new Error( + i18n.translate('discover.savedSearchEmbeddable.legacyURLConflict.errorMessage', { + defaultMessage: `This {type} has the same URL as a legacy alias. Disable the alias to resolve this error : {json}`, + values: { + type: SAVED_SEARCH_TYPE, + json: JSON.stringify({ + sourceId: savedSearch.id, + targetType: SAVED_SEARCH_TYPE, + targetSpace: ((await services.spaces?.getActiveSpace()) ?? {}).id || 'default', + }), + }, + }) + ); + } + const indexPattern = savedSearch.searchSource.getField('index'); const { executeTriggerActions } = await this.getStartServices(); const { SavedSearchEmbeddable: SavedSearchEmbeddableClass } = await import( diff --git a/src/plugins/discover/public/saved_searches/index.ts b/src/plugins/discover/public/saved_searches/index.ts index 30d4decdd5dca..fddba13242449 100644 --- a/src/plugins/discover/public/saved_searches/index.ts +++ b/src/plugins/discover/public/saved_searches/index.ts @@ -9,11 +9,17 @@ import { createSavedSearchesLoader } from './legacy/saved_searches'; export { getSavedSearch } from './get_saved_searches'; -export { getSavedSearchUrl, getSavedSearchFullPathUrl } from './saved_searches_utils'; +export { + getSavedSearchUrl, + getSavedSearchFullPathUrl, + savedSearchHasUrlConflict, +} from './saved_searches_utils'; export { useSavedSearchAliasMatchRedirect } from './saved_search_alias_match_redirect'; export { SavedSearchURLConflictCallout } from './saved_search_url_conflict_callout'; export { saveSavedSearch, SaveSavedSearchOptions } from './save_saved_searches'; +export { SAVED_SEARCH_TYPE } from './constants'; + export type { SavedSearch } from './types'; export type { LegacySavedSearch, SavedSearchLoader, SortOrder } from './legacy/types'; diff --git a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts index 1ea56de62d2e1..d82a3a658fa3a 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import { getSavedSearchUrl } from './saved_searches_utils'; +import { getSavedSearchUrl, savedSearchHasUrlConflict } from './saved_searches_utils'; import { SAVED_SEARCH_TYPE } from './constants'; import type { SavedSearch } from './types'; @@ -22,7 +22,7 @@ export const SavedSearchURLConflictCallout = ({ savedSearch, spaces, }: SavedSearchURLConflictCalloutProps) => { - if (spaces && savedSearch?.id && savedSearch?.sharingSavedObject?.outcome === 'conflict') { + if (spaces && savedSearch?.id && savedSearchHasUrlConflict(savedSearch)) { const otherObjectId = savedSearch.sharingSavedObject?.aliasTargetId; if (otherObjectId) { diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts index a24413f79d6b0..737c16b6522df 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts @@ -12,6 +12,9 @@ export const getSavedSearchUrl = (id?: string) => (id ? `#/view/${encodeURICompo export const getSavedSearchFullPathUrl = (id?: string) => `/app/discover${getSavedSearchUrl(id)}`; +export const savedSearchHasUrlConflict = (savedSearch: SavedSearch) => + savedSearch?.sharingSavedObject?.outcome === 'conflict'; + export const fromSavedSearchAttributes = ( id: string, attributes: SavedSearchAttributes, From 03214e36a6f6172ec26dca34d78a16bcd3c74cd2 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 5 Oct 2021 16:11:27 +0300 Subject: [PATCH 07/18] fix jest --- .../apps/main/services/use_discover_state.test.ts | 14 ++++++++++++++ .../apps/main/utils/get_fetch_observable.ts | 1 + 2 files changed, 15 insertions(+) diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts index 28f5f96acc144..c719f83980aa0 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts @@ -15,6 +15,20 @@ import { indexPatternMock } from '../../../../__mocks__/index_pattern'; import { SearchSource } from '../../../../../../data/common'; describe('test useDiscoverState', () => { + const originalSavedObjectsClient = discoverServiceMock.core.savedObjects.client; + + beforeAll(() => { + discoverServiceMock.core.savedObjects.client.resolve = jest.fn().mockReturnValue({ + saved_object: { + attributes: {}, + }, + }); + }); + + afterAll(() => { + discoverServiceMock.core.savedObjects.client = originalSavedObjectsClient; + }); + test('return is valid', async () => { const { history } = createSearchSessionMock(); diff --git a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts index 8720b3cf9ab50..de79a9425f17c 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_fetch_observable.ts @@ -34,6 +34,7 @@ export function getFetch$({ refetch$: DataRefetch$; searchSessionManager: DiscoverSearchSessionManager; searchSource: ISearchSource; + initialFetchStatus: FetchStatus; }) { const { timefilter } = data.query.timefilter; const { filterManager } = data.query; From 64c29b66b431c2e9020056e53d3f1a9b7136479b Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 5 Oct 2021 17:48:27 +0300 Subject: [PATCH 08/18] add DiscoverError component --- .../apps/main/discover_main_route.tsx | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx index e8d3fd81e2e74..001a3a2db3a6a 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx @@ -8,6 +8,9 @@ import React, { useEffect, useState, memo } from 'react'; import { History } from 'history'; import { useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { EuiEmptyPrompt } from '@elastic/eui'; + import { IndexPatternAttributes, ISearchSource, SavedObject } from 'src/plugins/data/common'; import { DiscoverServices } from '../../../build_services'; import { SavedSearch, getSavedSearch, getSavedSearchFullPathUrl } from '../../../saved_searches'; @@ -36,6 +39,21 @@ interface DiscoverLandingParams { id: string; } +const DiscoverError = ({ error }: { error: Error }) => ( + + {i18n.translate('discover.discoverError.title', { + defaultMessage: 'Error loading Discover', + })} + + } + body={

{error.message}

} + /> +); + export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { const { core, @@ -45,7 +63,7 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { toastNotifications, http: { basePath }, } = services; - + const [error, setError] = useState(); const [savedSearch, setSavedSearch] = useState(); const indexPattern = savedSearch?.searchSource?.getField('index'); const [indexPatternList, setIndexPatternList] = useState< @@ -58,16 +76,21 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { const savedSearchId = id; async function loadDefaultOrCurrentIndexPattern(searchSource: ISearchSource) { - await data.indexPatterns.ensureDefaultDataView(); - const { appStateContainer } = getState({ history, uiSettings: config }); - const { index } = appStateContainer.getState(); - const ip = await loadIndexPattern(index || '', data.indexPatterns, config); - const ipList = ip.list as Array>; - const indexPatternData = await resolveIndexPattern(ip, searchSource, toastNotifications); + try { + await data.indexPatterns.ensureDefaultDataView(); + const { appStateContainer } = getState({ history, uiSettings: config }); + const { index } = appStateContainer.getState(); + const ip = await loadIndexPattern(index || '', data.indexPatterns, config); - setIndexPatternList(ipList); + const ipList = ip.list as Array>; + const indexPatternData = await resolveIndexPattern(ip, searchSource, toastNotifications); - return indexPatternData; + setIndexPatternList(ipList); + + return indexPatternData; + } catch (e) { + setError(e); + } } async function loadSavedSearch() { @@ -136,6 +159,10 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { ); }, [chrome, savedSearch]); + if (error) { + return ; + } + if (!indexPattern || !savedSearch) { return ; } From 21af3ec6dab7f5d2b95217b2034973d5d2ec27ad Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 5 Oct 2021 19:14:59 +0300 Subject: [PATCH 09/18] fix Joe comments --- .../saved_searches/saved_search_alias_match_redirect.ts | 4 +--- .../saved_searches/saved_search_url_conflict_callout.ts | 4 +--- .../public/saved_visualizations/_saved_vis.ts | 6 ++++++ src/plugins/visualizations/public/vis.ts | 6 +++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts index a0448265725bb..aa82f9cfda829 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts @@ -9,7 +9,6 @@ import { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { getSavedSearchUrl } from './saved_searches_utils'; -import { SAVED_SEARCH_TYPE } from './constants'; import type { SavedSearch } from './types'; import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; @@ -32,10 +31,9 @@ export const useSavedSearchAliasMatchRedirect = ({ await spaces.ui.redirectLegacyUrl( getSavedSearchUrl(aliasTargetId), i18n.translate('discover.savedSearchAliasMatchRedirect.objectNoun', { - defaultMessage: '{savedSearch} {type}', + defaultMessage: '{savedSearch} search', values: { savedSearch: savedSearch.title, - type: SAVED_SEARCH_TYPE, }, }) ); diff --git a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts index d82a3a658fa3a..53f1462138979 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts @@ -8,7 +8,6 @@ import { i18n } from '@kbn/i18n'; import { getSavedSearchUrl, savedSearchHasUrlConflict } from './saved_searches_utils'; -import { SAVED_SEARCH_TYPE } from './constants'; import type { SavedSearch } from './types'; import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; @@ -28,10 +27,9 @@ export const SavedSearchURLConflictCallout = ({ if (otherObjectId) { return spaces.ui.components.getLegacyUrlConflict({ objectNoun: i18n.translate('discover.savedSearchURLConflictCallout.objectNoun', { - defaultMessage: '{savedSearch} {type}', + defaultMessage: '{savedSearch} search', values: { savedSearch: savedSearch.title, - type: SAVED_SEARCH_TYPE, }, }), currentObjectId: savedSearch.id, diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts index 90dd774c4acac..e6947ff493b82 100644 --- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts @@ -16,6 +16,7 @@ import type { SavedObjectsStart, SavedObject } from '../../../../plugins/saved_objects/public'; // @ts-ignore import { updateOldState } from '../legacy/vis_update_state'; +import { __LEGACY } from '../../../discover/public'; import { extractReferences, injectReferences } from './saved_visualization_references'; import type { SavedObjectsClientContract } from '../../../../core/public'; import type { IndexPatternsContract } from '../../../../plugins/data/public'; @@ -65,6 +66,8 @@ export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => { }; export function createSavedVisClass(services: SavedVisServices) { + const savedSearch = __LEGACY.createSavedSearchesLoader(services); + class SavedVis extends services.savedObjects.SavedObjectClass { public static type: string = 'visualization'; public static mapping: Record = { @@ -105,6 +108,9 @@ export function createSavedVisClass(services: SavedVisServices) { if (savedVis.searchSourceFields?.index) { await services.indexPatterns.get(savedVis.searchSourceFields.index as any); } + if (savedVis.savedSearchId) { + await savedSearch.get(savedVis.savedSearchId); + } return savedVis as any as SavedObject; }, }); diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index b879b683bf7b0..7c1aecd31c702 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -61,13 +61,13 @@ export interface VisData { const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: string) => { if (savedSearchId) { - const savedSearchSearch = await getSavedSearch(savedSearchId, { + const savedSearch = await getSavedSearch(savedSearchId, { search: getSearch(), savedObjectsClient: getSavedObjects().client, }); - if (savedSearchSearch?.searchSource) { - return savedSearchSearch?.searchSource; + if (savedSearch?.searchSource) { + inputSearchSource.setParent(savedSearch.searchSource); } } return inputSearchSource; From eaa583bed96f847dfb3fdba90871368302b025c3 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 5 Oct 2021 22:35:25 +0300 Subject: [PATCH 10/18] add search params --- .../components/layout/discover_layout.tsx | 22 ++++++++++++++----- .../saved_search_alias_match_redirect.test.ts | 19 ++++++++++++---- .../saved_search_alias_match_redirect.ts | 7 ++++-- ...saved_search_url_conflict_callout.test.tsx | 13 +++++++++-- .../saved_search_url_conflict_callout.ts | 5 ++++- 5 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index 03b055d2ea847..6c6393daf30a3 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -69,10 +69,18 @@ export function DiscoverLayout({ state, stateContainer, }: DiscoverLayoutProps) { - const { trackUiMetric, capabilities, indexPatterns, data, uiSettings, filterManager, storage } = - services; + const { + trackUiMetric, + capabilities, + indexPatterns, + data, + uiSettings, + filterManager, + storage, + history, + spaces, + } = services; const { main$, charts$, totalHits$ } = savedSearchData$; - const [expandedDoc, setExpandedDoc] = useState(undefined); const [inspectorSession, setInspectorSession] = useState(undefined); const fetchCounter = useRef(0); @@ -84,7 +92,7 @@ export function DiscoverLayout({ } }, [dataState.fetchStatus]); - useSavedSearchAliasMatchRedirect({ savedSearch, spaces: services.spaces }); + useSavedSearchAliasMatchRedirect({ savedSearch, spaces, history }); const timeField = useMemo(() => { return indexPattern.type !== 'rollup' ? indexPattern.timeFieldName : undefined; @@ -180,7 +188,11 @@ export function DiscoverLayout({ resetSavedSearch={resetSavedSearch} /> - +

{savedSearch.title}

diff --git a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts index c5e706a76c1c0..a8ac0e5cf137d 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts @@ -7,6 +7,7 @@ */ import { renderHook } from '@testing-library/react-hooks'; +import type { History } from 'history'; import { useSavedSearchAliasMatchRedirect } from './saved_search_alias_match_redirect'; import type { SavedSearch } from './types'; @@ -15,9 +16,16 @@ import { spacesPluginMock } from '../../../../../x-pack/plugins/spaces/public/mo describe('useSavedSearchAliasMatchRedirect', () => { let spaces: ReturnType; + let history: () => History; beforeEach(() => { spaces = spacesPluginMock.createStartContract(); + history = () => + ({ + location: { + search: '?_g=foo', + }, + } as History); }); test('should redirect in case of aliasMatch', () => { @@ -29,9 +37,12 @@ describe('useSavedSearchAliasMatchRedirect', () => { }, } as SavedSearch; - renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch })); + renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch, history })); - expect(spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith('#/view/aliasTargetId', ' search'); + expect(spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith( + '#/view/aliasTargetId?_g=foo', + ' search' + ); }); test('should not redirect if outcome !== aliasMatch', () => { @@ -42,7 +53,7 @@ describe('useSavedSearchAliasMatchRedirect', () => { }, } as SavedSearch; - renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch })); + renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch, history })); expect(spaces.ui.redirectLegacyUrl).not.toHaveBeenCalled(); }); @@ -55,7 +66,7 @@ describe('useSavedSearchAliasMatchRedirect', () => { }, } as SavedSearch; - renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch })); + renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch, history })); expect(spaces.ui.redirectLegacyUrl).not.toHaveBeenCalled(); }); diff --git a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts index aa82f9cfda829..626a1d0fc8b5f 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { History } from 'history'; import { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { getSavedSearchUrl } from './saved_searches_utils'; @@ -16,11 +17,13 @@ import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; interface SavedSearchAliasMatchRedirectProps { savedSearch?: SavedSearch; spaces?: SpacesApi; + history: () => History; } export const useSavedSearchAliasMatchRedirect = ({ savedSearch, spaces, + history, }: SavedSearchAliasMatchRedirectProps) => { useEffect(() => { async function aliasMatchRedirect() { @@ -29,7 +32,7 @@ export const useSavedSearchAliasMatchRedirect = ({ if (spaces && aliasTargetId && outcome === 'aliasMatch') { await spaces.ui.redirectLegacyUrl( - getSavedSearchUrl(aliasTargetId), + `${getSavedSearchUrl(aliasTargetId)}${history().location.search}`, i18n.translate('discover.savedSearchAliasMatchRedirect.objectNoun', { defaultMessage: '{savedSearch} search', values: { @@ -42,5 +45,5 @@ export const useSavedSearchAliasMatchRedirect = ({ } aliasMatchRedirect(); - }, [savedSearch, spaces]); + }, [savedSearch, spaces, history]); }; diff --git a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx index e50c03765ae29..ecb85f600d56a 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx +++ b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx @@ -7,6 +7,8 @@ */ import React from 'react'; +import type { History } from 'history'; + import { mountWithIntl } from '@kbn/test/jest'; import { SavedSearchURLConflictCallout } from './saved_search_url_conflict_callout'; import type { SavedSearch } from './types'; @@ -15,10 +17,17 @@ import { spacesPluginMock } from '../../../../../x-pack/plugins/spaces/public/mo describe('SavedSearchURLConflictCallout', () => { let spaces: ReturnType; + let history: () => History; beforeEach(() => { spaces = spacesPluginMock.createStartContract(); spaces.ui.components.getLegacyUrlConflict = jest.fn().mockReturnValue('callout'); + history = () => + ({ + location: { + search: '?_g=foo', + }, + } as History); }); test("should render URLConflictCallout in case of id's conflicts", () => { @@ -31,7 +40,7 @@ describe('SavedSearchURLConflictCallout', () => { } as SavedSearch; const component = mountWithIntl( - + ); expect(component.children()).toMatchInlineSnapshot(`"callout"`); @@ -44,7 +53,7 @@ describe('SavedSearchURLConflictCallout', () => { } as SavedSearch; const component = mountWithIntl( - + ); expect(component.children()).toMatchInlineSnapshot(`null`); diff --git a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts index 53f1462138979..eb1c3d5919149 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts @@ -7,6 +7,7 @@ */ import { i18n } from '@kbn/i18n'; +import type { History } from 'history'; import { getSavedSearchUrl, savedSearchHasUrlConflict } from './saved_searches_utils'; import type { SavedSearch } from './types'; @@ -15,11 +16,13 @@ import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; interface SavedSearchURLConflictCalloutProps { savedSearch?: SavedSearch; spaces?: SpacesApi; + history: () => History; } export const SavedSearchURLConflictCallout = ({ savedSearch, spaces, + history, }: SavedSearchURLConflictCalloutProps) => { if (spaces && savedSearch?.id && savedSearchHasUrlConflict(savedSearch)) { const otherObjectId = savedSearch.sharingSavedObject?.aliasTargetId; @@ -33,7 +36,7 @@ export const SavedSearchURLConflictCallout = ({ }, }), currentObjectId: savedSearch.id, - otherObjectPath: getSavedSearchUrl(otherObjectId), + otherObjectPath: `${getSavedSearchUrl(otherObjectId)}${history().location.search}`, otherObjectId, }); } From 88e517ebf67bdf7d35a1c1ea7c405d978b2df947 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 5 Oct 2021 23:24:33 +0300 Subject: [PATCH 11/18] add throwErrorOnUrlConflict util method --- .../embeddable/search_embeddable_factory.ts | 17 ++------------ src/plugins/discover/public/index.ts | 1 + .../discover/public/saved_searches/index.ts | 1 + .../saved_searches_utils.test.ts | 22 +++++++++++++++++++ .../saved_searches/saved_searches_utils.ts | 21 +++++++++++++++++- 5 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts index efb24384a8ae9..dab30284be550 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts @@ -24,6 +24,7 @@ import { getSavedSearch, getSavedSearchUrl, savedSearchHasUrlConflict, + throwErrorOnUrlConflict, SAVED_SEARCH_TYPE, } from '../../saved_searches'; @@ -75,21 +76,7 @@ export class SearchEmbeddableFactory savedObjectsClient: services.core.savedObjects.client, }); - if (savedSearchHasUrlConflict(savedSearch)) { - throw new Error( - i18n.translate('discover.savedSearchEmbeddable.legacyURLConflict.errorMessage', { - defaultMessage: `This {type} has the same URL as a legacy alias. Disable the alias to resolve this error : {json}`, - values: { - type: SAVED_SEARCH_TYPE, - json: JSON.stringify({ - sourceId: savedSearch.id, - targetType: SAVED_SEARCH_TYPE, - targetSpace: ((await services.spaces?.getActiveSpace()) ?? {}).id || 'default', - }), - }, - }) - ); - } + await throwErrorOnUrlConflict(savedSearch, services.spaces); const indexPattern = savedSearch.searchSource.getField('index'); const { executeTriggerActions } = await this.getStartServices(); diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 5215b67784c88..f7373358f471f 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -13,6 +13,7 @@ export { getSavedSearch, getSavedSearchFullPathUrl, getSavedSearchUrl, + throwErrorOnUrlConflict, SavedSearch, LegacySavedSearch, SavedSearchLoader, diff --git a/src/plugins/discover/public/saved_searches/index.ts b/src/plugins/discover/public/saved_searches/index.ts index fddba13242449..8388d1d3e1897 100644 --- a/src/plugins/discover/public/saved_searches/index.ts +++ b/src/plugins/discover/public/saved_searches/index.ts @@ -13,6 +13,7 @@ export { getSavedSearchUrl, getSavedSearchFullPathUrl, savedSearchHasUrlConflict, + throwErrorOnUrlConflict, } from './saved_searches_utils'; export { useSavedSearchAliasMatchRedirect } from './saved_search_alias_match_redirect'; export { SavedSearchURLConflictCallout } from './saved_search_url_conflict_callout'; diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts index 1e88e488ef2d6..56cf3543073c8 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts @@ -11,6 +11,7 @@ import { getSavedSearchFullPathUrl, fromSavedSearchAttributes, toSavedSearchAttributes, + throwErrorOnUrlConflict, } from './saved_searches_utils'; import { createSearchSourceMock } from '../../../data/public/mocks'; @@ -78,6 +79,27 @@ describe('saved_searches_utils', () => { }); }); + describe('throwErrorOnUrlConflict', () => { + test('should throw an error on url conflict', async () => { + let error = 'no error'; + + try { + await throwErrorOnUrlConflict({ + id: 'id', + sharingSavedObject: { + outcome: 'conflict', + }, + } as SavedSearch); + } catch (e) { + error = e.message; + } + + expect(error).toBe( + 'This search has the same URL as a legacy alias. Disable the alias to resolve this error : {"sourceId":"id","targetType":"search","targetSpace":"default"}' + ); + }); + }); + describe('toSavedSearchAttributes', () => { test('should serialize SavedSearch attributes', () => { const savedSearch: SavedSearch = { diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts index 737c16b6522df..a278c716f95bf 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts @@ -5,8 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import { i18n } from '@kbn/i18n'; +import { SAVED_SEARCH_TYPE } from './constants'; import type { SavedSearchAttributes, SavedSearch } from './types'; +import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; export const getSavedSearchUrl = (id?: string) => (id ? `#/view/${encodeURIComponent(id)}` : '#/'); @@ -15,6 +17,23 @@ export const getSavedSearchFullPathUrl = (id?: string) => `/app/discover${getSav export const savedSearchHasUrlConflict = (savedSearch: SavedSearch) => savedSearch?.sharingSavedObject?.outcome === 'conflict'; +export const throwErrorOnUrlConflict = async (savedSearch: SavedSearch, spaces?: SpacesApi) => { + if (savedSearchHasUrlConflict(savedSearch)) { + throw new Error( + i18n.translate('discover.savedSearchEmbeddable.legacyURLConflict.errorMessage', { + defaultMessage: `This search has the same URL as a legacy alias. Disable the alias to resolve this error : {json}`, + values: { + json: JSON.stringify({ + sourceId: savedSearch.id, + targetType: SAVED_SEARCH_TYPE, + targetSpace: ((await spaces?.getActiveSpace()) ?? {}).id || 'default', + }), + }, + }) + ); + } +}; + export const fromSavedSearchAttributes = ( id: string, attributes: SavedSearchAttributes, From 465625fc81bf6e71d4d826fad997aec1743bc587 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 5 Oct 2021 23:57:58 +0300 Subject: [PATCH 12/18] add error handling into transform plugin --- .../embeddable/search_embeddable_factory.ts | 6 ++-- src/plugins/discover/public/index.ts | 4 ++- .../discover/public/saved_searches/index.ts | 3 +- .../saved_searches_utils.test.ts | 6 ++-- .../saved_searches/saved_searches_utils.ts | 33 +++++++++++-------- x-pack/plugins/transform/kibana.json | 3 +- .../transform/public/app/app_dependencies.tsx | 2 ++ .../use_search_items/use_search_items.ts | 11 ++++++- .../public/app/mount_management_section.ts | 3 +- x-pack/plugins/transform/public/plugin.ts | 2 ++ .../transform/public/shared_imports.ts | 6 +++- 11 files changed, 53 insertions(+), 26 deletions(-) diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts index dab30284be550..7bf1bc01aad2f 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts @@ -23,9 +23,7 @@ import { SavedSearchEmbeddable } from './saved_search_embeddable'; import { getSavedSearch, getSavedSearchUrl, - savedSearchHasUrlConflict, - throwErrorOnUrlConflict, - SAVED_SEARCH_TYPE, + throwErrorOnSavedSearchUrlConflict, } from '../../saved_searches'; interface StartServices { @@ -76,7 +74,7 @@ export class SearchEmbeddableFactory savedObjectsClient: services.core.savedObjects.client, }); - await throwErrorOnUrlConflict(savedSearch, services.spaces); + await throwErrorOnSavedSearchUrlConflict(savedSearch, services.spaces); const indexPattern = savedSearch.searchSource.getField('index'); const { executeTriggerActions } = await this.getStartServices(); diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index f7373358f471f..9f72b3f554f30 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -13,7 +13,9 @@ export { getSavedSearch, getSavedSearchFullPathUrl, getSavedSearchUrl, - throwErrorOnUrlConflict, + savedSearchHasUrlConflict, + getSavedSearchUrlConflictMessage, + throwErrorOnSavedSearchUrlConflict, SavedSearch, LegacySavedSearch, SavedSearchLoader, diff --git a/src/plugins/discover/public/saved_searches/index.ts b/src/plugins/discover/public/saved_searches/index.ts index 8388d1d3e1897..012050c8f4c41 100644 --- a/src/plugins/discover/public/saved_searches/index.ts +++ b/src/plugins/discover/public/saved_searches/index.ts @@ -13,7 +13,8 @@ export { getSavedSearchUrl, getSavedSearchFullPathUrl, savedSearchHasUrlConflict, - throwErrorOnUrlConflict, + getSavedSearchUrlConflictMessage, + throwErrorOnSavedSearchUrlConflict, } from './saved_searches_utils'; export { useSavedSearchAliasMatchRedirect } from './saved_search_alias_match_redirect'; export { SavedSearchURLConflictCallout } from './saved_search_url_conflict_callout'; diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts index 56cf3543073c8..16f53ba60cd96 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts @@ -11,7 +11,7 @@ import { getSavedSearchFullPathUrl, fromSavedSearchAttributes, toSavedSearchAttributes, - throwErrorOnUrlConflict, + throwErrorOnSavedSearchUrlConflict, } from './saved_searches_utils'; import { createSearchSourceMock } from '../../../data/public/mocks'; @@ -79,12 +79,12 @@ describe('saved_searches_utils', () => { }); }); - describe('throwErrorOnUrlConflict', () => { + describe('throwErrorOnSavedSearchUrlConflict', () => { test('should throw an error on url conflict', async () => { let error = 'no error'; try { - await throwErrorOnUrlConflict({ + await throwErrorOnSavedSearchUrlConflict({ id: 'id', sharingSavedObject: { outcome: 'conflict', diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts index a278c716f95bf..225347a0a452f 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts @@ -17,20 +17,27 @@ export const getSavedSearchFullPathUrl = (id?: string) => `/app/discover${getSav export const savedSearchHasUrlConflict = (savedSearch: SavedSearch) => savedSearch?.sharingSavedObject?.outcome === 'conflict'; -export const throwErrorOnUrlConflict = async (savedSearch: SavedSearch, spaces?: SpacesApi) => { +export const getSavedSearchUrlConflictMessage = async ( + savedSearch: SavedSearch, + spaces?: SpacesApi +) => + i18n.translate('discover.savedSearchEmbeddable.legacyURLConflict.errorMessage', { + defaultMessage: `This search has the same URL as a legacy alias. Disable the alias to resolve this error : {json}`, + values: { + json: JSON.stringify({ + sourceId: savedSearch.id, + targetType: SAVED_SEARCH_TYPE, + targetSpace: ((await spaces?.getActiveSpace()) ?? {}).id || 'default', + }), + }, + }); + +export const throwErrorOnSavedSearchUrlConflict = async ( + savedSearch: SavedSearch, + spaces?: SpacesApi +) => { if (savedSearchHasUrlConflict(savedSearch)) { - throw new Error( - i18n.translate('discover.savedSearchEmbeddable.legacyURLConflict.errorMessage', { - defaultMessage: `This search has the same URL as a legacy alias. Disable the alias to resolve this error : {json}`, - values: { - json: JSON.stringify({ - sourceId: savedSearch.id, - targetType: SAVED_SEARCH_TYPE, - targetSpace: ((await spaces?.getActiveSpace()) ?? {}).id || 'default', - }), - }, - }) - ); + throw new Error(await getSavedSearchUrlConflictMessage(savedSearch, spaces)); } }; diff --git a/x-pack/plugins/transform/kibana.json b/x-pack/plugins/transform/kibana.json index c9f6beeee5aff..418fa5070fcb9 100644 --- a/x-pack/plugins/transform/kibana.json +++ b/x-pack/plugins/transform/kibana.json @@ -14,7 +14,8 @@ ], "optionalPlugins": [ "security", - "usageCollection" + "usageCollection", + "spaces" ], "configPath": ["xpack", "transform"], "requiredBundles": [ diff --git a/x-pack/plugins/transform/public/app/app_dependencies.tsx b/x-pack/plugins/transform/public/app/app_dependencies.tsx index c39aa5a49e5e9..d3f356f3e83b3 100644 --- a/x-pack/plugins/transform/public/app/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/app_dependencies.tsx @@ -10,6 +10,7 @@ import type { DataPublicPluginStart } from 'src/plugins/data/public'; import type { SavedObjectsStart } from 'src/plugins/saved_objects/public'; import type { ScopedHistory } from 'kibana/public'; import type { SharePluginStart } from 'src/plugins/share/public'; +import type { SpacesPluginStart } from '../../../spaces/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import type { Storage } from '../../../../../src/plugins/kibana_utils/public'; @@ -32,6 +33,7 @@ export interface AppDependencies { savedObjectsPlugin: SavedObjectsStart; share: SharePluginStart; ml: GetMlSharedImportsReturnType; + spaces?: SpacesPluginStart; } export const useAppDependencies = () => { diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index 28cff69a6ef90..547527cdf2d33 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -11,7 +11,11 @@ import { i18n } from '@kbn/i18n'; import { isIndexPattern } from '../../../../common/types/index_pattern'; -import { getSavedSearch } from '../../../shared_imports'; +import { + getSavedSearch, + getSavedSearchUrlConflictMessage, + savedSearchHasUrlConflict, +} from '../../../shared_imports'; import { useAppDependencies } from '../../app_dependencies'; @@ -51,6 +55,11 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { search: appDeps.data.search, savedObjectsClient: appDeps.savedObjects.client, }); + + if (savedSearchHasUrlConflict(fetchedSavedSearch)) { + setError(await getSavedSearchUrlConflictMessage(fetchedSavedSearch, appDeps.spaces)); + return; + } } catch (e) { // Just let fetchedSavedSearch stay undefined in case it doesn't exist. } diff --git a/x-pack/plugins/transform/public/app/mount_management_section.ts b/x-pack/plugins/transform/public/app/mount_management_section.ts index 1d39d233f8284..1747330818547 100644 --- a/x-pack/plugins/transform/public/app/mount_management_section.ts +++ b/x-pack/plugins/transform/public/app/mount_management_section.ts @@ -29,7 +29,7 @@ export async function mountManagementSection( const startServices = await getStartServices(); const [core, plugins] = startServices; const { application, chrome, docLinks, i18n, overlays, savedObjects, uiSettings } = core; - const { data, share } = plugins; + const { data, share, spaces } = plugins; const { docTitle } = chrome; // Initialize services @@ -53,6 +53,7 @@ export async function mountManagementSection( history, savedObjectsPlugin: plugins.savedObjects, share, + spaces, ml: await getMlSharedImports(), }; diff --git a/x-pack/plugins/transform/public/plugin.ts b/x-pack/plugins/transform/public/plugin.ts index b058be46d677b..c967b901e91a1 100644 --- a/x-pack/plugins/transform/public/plugin.ts +++ b/x-pack/plugins/transform/public/plugin.ts @@ -13,6 +13,7 @@ import type { HomePublicPluginSetup } from 'src/plugins/home/public'; import type { SavedObjectsStart } from 'src/plugins/saved_objects/public'; import type { ManagementSetup } from 'src/plugins/management/public'; import type { SharePluginStart } from 'src/plugins/share/public'; +import type { SpacesApi } from '../../spaces/public'; import { registerFeature } from './register_feature'; export interface PluginsDependencies { @@ -21,6 +22,7 @@ export interface PluginsDependencies { home: HomePublicPluginSetup; savedObjects: SavedObjectsStart; share: SharePluginStart; + spaces?: SpacesApi; } export class TransformUiPlugin { diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index b3662466a53f5..30d6412ab53df 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -7,7 +7,11 @@ export { XJsonMode } from '@kbn/ace'; export { UseRequestConfig, useRequest } from '../../../../src/plugins/es_ui_shared/public'; -export { getSavedSearch } from '../../../../src/plugins/discover/public'; +export { + getSavedSearch, + getSavedSearchUrlConflictMessage, + savedSearchHasUrlConflict, +} from '../../../../src/plugins/discover/public'; export { getMlSharedImports, From e93c44e6130e62e7414d2e88662bdcc034492dcb Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 6 Oct 2021 13:54:00 +0300 Subject: [PATCH 13/18] do some updates --- .../apps/main/discover_main_route.tsx | 1 + .../apps/main/services/use_discover_state.ts | 1 + .../embeddable/search_embeddable_factory.ts | 3 +- src/plugins/discover/public/index.ts | 1 - .../saved_searches/get_saved_searches.test.ts | 3 +- .../saved_searches/get_saved_searches.ts | 13 ++++++++- .../discover/public/saved_searches/index.ts | 1 - .../save_saved_searches.test.ts | 2 +- .../saved_search_alias_match_redirect.test.ts | 6 ++-- .../saved_search_alias_match_redirect.ts | 2 +- ...saved_search_url_conflict_callout.test.tsx | 4 +-- .../saved_search_url_conflict_callout.ts | 6 ++-- .../saved_searches_utils.test.ts | 7 +++-- .../saved_searches/saved_searches_utils.ts | 29 +++++-------------- .../discover/public/saved_searches/types.ts | 3 +- src/plugins/visualizations/public/vis.ts | 4 ++- .../utils/get_visualization_instance.ts | 8 ++++- .../use_search_items/use_search_items.ts | 11 +++---- .../transform/public/shared_imports.ts | 1 - 19 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx index 001a3a2db3a6a..366c3d1ab04f6 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx @@ -98,6 +98,7 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { const currentSavedSearch = await getSavedSearch(savedSearchId, { search: services.data.search, savedObjectsClient: core.savedObjects.client, + spaces: services.spaces, }); const loadedIndexPattern = await loadDefaultOrCurrentIndexPattern( diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts index 17fcc0b26566a..ce30c0749b938 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -151,6 +151,7 @@ export function useDiscoverState({ const newSavedSearch = await getSavedSearch(id, { search: services.data.search, savedObjectsClient: services.core.savedObjects.client, + spaces: services.spaces, }); const newIndexPattern = newSavedSearch.searchSource.getField('index') || indexPattern; diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts index 7bf1bc01aad2f..a8b492d368768 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts @@ -72,9 +72,10 @@ export class SearchEmbeddableFactory const savedSearch = await getSavedSearch(savedObjectId, { search: services.data.search, savedObjectsClient: services.core.savedObjects.client, + spaces: services.spaces, }); - await throwErrorOnSavedSearchUrlConflict(savedSearch, services.spaces); + await throwErrorOnSavedSearchUrlConflict(savedSearch); const indexPattern = savedSearch.searchSource.getField('index'); const { executeTriggerActions } = await this.getStartServices(); diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 9f72b3f554f30..f6cd687c962c3 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -13,7 +13,6 @@ export { getSavedSearch, getSavedSearchFullPathUrl, getSavedSearchUrl, - savedSearchHasUrlConflict, getSavedSearchUrlConflictMessage, throwErrorOnSavedSearchUrlConflict, SavedSearch, diff --git a/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts b/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts index 6306938ab4e38..755831e7009ed 100644 --- a/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts +++ b/src/plugins/discover/public/saved_searches/get_saved_searches.test.ts @@ -126,8 +126,9 @@ describe('getSavedSearch', () => { "setParent": [MockFunction], "setPreferredSearchStrategyId": [MockFunction], }, - "sharingSavedObject": Object { + "sharingSavedObjectProps": Object { "aliasTargetId": undefined, + "errorJSON": undefined, "outcome": "exactMatch", }, "sort": Array [ diff --git a/src/plugins/discover/public/saved_searches/get_saved_searches.ts b/src/plugins/discover/public/saved_searches/get_saved_searches.ts index 5d37d7dcfc510..e1e04d0a5ee25 100644 --- a/src/plugins/discover/public/saved_searches/get_saved_searches.ts +++ b/src/plugins/discover/public/saved_searches/get_saved_searches.ts @@ -15,9 +15,12 @@ import { fromSavedSearchAttributes } from './saved_searches_utils'; import { injectSearchSourceReferences, parseSearchSourceJSON } from '../../../data/public'; import { SavedObjectNotFound } from '../../../kibana_utils/public'; +import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; + interface GetSavedSearchDependencies { search: DataPublicPluginStart['search']; savedObjectsClient: SavedObjectsStart['client']; + spaces?: SpacesApi; } const getEmptySavedSearch = ({ @@ -30,7 +33,7 @@ const getEmptySavedSearch = ({ const findSavedSearch = async ( savedSearchId: string, - { search, savedObjectsClient }: GetSavedSearchDependencies + { search, savedObjectsClient, spaces }: GetSavedSearchDependencies ) => { const so = await savedObjectsClient.resolve( SAVED_SEARCH_TYPE, @@ -59,6 +62,14 @@ const findSavedSearch = async ( { outcome: so.outcome, aliasTargetId: so.alias_target_id, + errorJSON: + so.outcome === 'conflict' && spaces + ? JSON.stringify({ + targetType: SAVED_SEARCH_TYPE, + sourceId: savedSearchId, + targetSpace: (await spaces!.getActiveSpace()).id, + }) + : undefined, } ); }; diff --git a/src/plugins/discover/public/saved_searches/index.ts b/src/plugins/discover/public/saved_searches/index.ts index 012050c8f4c41..6870fa5e6d617 100644 --- a/src/plugins/discover/public/saved_searches/index.ts +++ b/src/plugins/discover/public/saved_searches/index.ts @@ -12,7 +12,6 @@ export { getSavedSearch } from './get_saved_searches'; export { getSavedSearchUrl, getSavedSearchFullPathUrl, - savedSearchHasUrlConflict, getSavedSearchUrlConflictMessage, throwErrorOnSavedSearchUrlConflict, } from './saved_searches_utils'; diff --git a/src/plugins/discover/public/saved_searches/save_saved_searches.test.ts b/src/plugins/discover/public/saved_searches/save_saved_searches.test.ts index bf995fffa5f71..eabbfe7f9419f 100644 --- a/src/plugins/discover/public/saved_searches/save_saved_searches.test.ts +++ b/src/plugins/discover/public/saved_searches/save_saved_searches.test.ts @@ -32,7 +32,7 @@ describe('saveSavedSearch', () => { references: [], }), }, - sharingSavedObject: { + sharingSavedObjectProps: { outcome: 'aliasMatch', }, } as SavedSearch; diff --git a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts index a8ac0e5cf137d..0a871061d2b19 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.test.ts @@ -31,7 +31,7 @@ describe('useSavedSearchAliasMatchRedirect', () => { test('should redirect in case of aliasMatch', () => { const savedSearch = { id: 'id', - sharingSavedObject: { + sharingSavedObjectProps: { outcome: 'aliasMatch', aliasTargetId: 'aliasTargetId', }, @@ -48,7 +48,7 @@ describe('useSavedSearchAliasMatchRedirect', () => { test('should not redirect if outcome !== aliasMatch', () => { const savedSearch = { id: 'id', - sharingSavedObject: { + sharingSavedObjectProps: { outcome: 'exactMatch', }, } as SavedSearch; @@ -61,7 +61,7 @@ describe('useSavedSearchAliasMatchRedirect', () => { test('should not redirect if aliasTargetId is not defined', () => { const savedSearch = { id: 'id', - sharingSavedObject: { + sharingSavedObjectProps: { outcome: 'aliasMatch', }, } as SavedSearch; diff --git a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts index 626a1d0fc8b5f..3a88c1a2b1989 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_alias_match_redirect.ts @@ -28,7 +28,7 @@ export const useSavedSearchAliasMatchRedirect = ({ useEffect(() => { async function aliasMatchRedirect() { if (savedSearch) { - const { aliasTargetId, outcome } = savedSearch.sharingSavedObject ?? {}; + const { aliasTargetId, outcome } = savedSearch.sharingSavedObjectProps ?? {}; if (spaces && aliasTargetId && outcome === 'aliasMatch') { await spaces.ui.redirectLegacyUrl( diff --git a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx index ecb85f600d56a..c92c15e771f64 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx +++ b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.test.tsx @@ -33,7 +33,7 @@ describe('SavedSearchURLConflictCallout', () => { test("should render URLConflictCallout in case of id's conflicts", () => { const savedSearch = { id: 'id', - sharingSavedObject: { + sharingSavedObjectProps: { outcome: 'conflict', aliasTargetId: 'aliasTargetId', }, @@ -49,7 +49,7 @@ describe('SavedSearchURLConflictCallout', () => { test('should not render URLConflictCallout in case of no conflicts', () => { const savedSearch = { id: 'id', - sharingSavedObject: {}, + sharingSavedObjectProps: {}, } as SavedSearch; const component = mountWithIntl( diff --git a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts index eb1c3d5919149..fd07126c496cf 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import type { History } from 'history'; -import { getSavedSearchUrl, savedSearchHasUrlConflict } from './saved_searches_utils'; +import { getSavedSearchUrl } from './saved_searches_utils'; import type { SavedSearch } from './types'; import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; @@ -24,8 +24,8 @@ export const SavedSearchURLConflictCallout = ({ spaces, history, }: SavedSearchURLConflictCalloutProps) => { - if (spaces && savedSearch?.id && savedSearchHasUrlConflict(savedSearch)) { - const otherObjectId = savedSearch.sharingSavedObject?.aliasTargetId; + if (spaces && savedSearch?.id && savedSearch?.sharingSavedObjectProps?.outcome === 'conflict') { + const otherObjectId = savedSearch.sharingSavedObjectProps?.aliasTargetId; if (otherObjectId) { return spaces.ui.components.getLegacyUrlConflict({ diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts index 16f53ba60cd96..12c73e86b3dc4 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.test.ts @@ -71,7 +71,7 @@ describe('saved_searches_utils', () => { "requestStartHandlers": Array [], "searchStrategyId": undefined, }, - "sharingSavedObject": Object {}, + "sharingSavedObjectProps": Object {}, "sort": Array [], "title": "saved search", } @@ -86,8 +86,9 @@ describe('saved_searches_utils', () => { try { await throwErrorOnSavedSearchUrlConflict({ id: 'id', - sharingSavedObject: { + sharingSavedObjectProps: { outcome: 'conflict', + errorJSON: '{}', }, } as SavedSearch); } catch (e) { @@ -95,7 +96,7 @@ describe('saved_searches_utils', () => { } expect(error).toBe( - 'This search has the same URL as a legacy alias. Disable the alias to resolve this error : {"sourceId":"id","targetType":"search","targetSpace":"default"}' + 'This search has the same URL as a legacy alias. Disable the alias to resolve this error : {}' ); }); }); diff --git a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts index 225347a0a452f..98ab2267a875e 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches_utils.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches_utils.ts @@ -6,38 +6,23 @@ * Side Public License, v 1. */ import { i18n } from '@kbn/i18n'; -import { SAVED_SEARCH_TYPE } from './constants'; import type { SavedSearchAttributes, SavedSearch } from './types'; -import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; export const getSavedSearchUrl = (id?: string) => (id ? `#/view/${encodeURIComponent(id)}` : '#/'); export const getSavedSearchFullPathUrl = (id?: string) => `/app/discover${getSavedSearchUrl(id)}`; -export const savedSearchHasUrlConflict = (savedSearch: SavedSearch) => - savedSearch?.sharingSavedObject?.outcome === 'conflict'; - -export const getSavedSearchUrlConflictMessage = async ( - savedSearch: SavedSearch, - spaces?: SpacesApi -) => +export const getSavedSearchUrlConflictMessage = async (savedSearch: SavedSearch) => i18n.translate('discover.savedSearchEmbeddable.legacyURLConflict.errorMessage', { defaultMessage: `This search has the same URL as a legacy alias. Disable the alias to resolve this error : {json}`, values: { - json: JSON.stringify({ - sourceId: savedSearch.id, - targetType: SAVED_SEARCH_TYPE, - targetSpace: ((await spaces?.getActiveSpace()) ?? {}).id || 'default', - }), + json: savedSearch.sharingSavedObjectProps?.errorJSON, }, }); -export const throwErrorOnSavedSearchUrlConflict = async ( - savedSearch: SavedSearch, - spaces?: SpacesApi -) => { - if (savedSearchHasUrlConflict(savedSearch)) { - throw new Error(await getSavedSearchUrlConflictMessage(savedSearch, spaces)); +export const throwErrorOnSavedSearchUrlConflict = async (savedSearch: SavedSearch) => { + if (savedSearch.sharingSavedObjectProps?.errorJSON) { + throw new Error(await getSavedSearchUrlConflictMessage(savedSearch)); } }; @@ -45,11 +30,11 @@ export const fromSavedSearchAttributes = ( id: string, attributes: SavedSearchAttributes, searchSource: SavedSearch['searchSource'], - sharingSavedObject: SavedSearch['sharingSavedObject'] + sharingSavedObjectProps: SavedSearch['sharingSavedObjectProps'] ): SavedSearch => ({ id, searchSource, - sharingSavedObject, + sharingSavedObjectProps, title: attributes.title, sort: attributes.sort, columns: attributes.columns, diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index 860d9fed02ac4..645ada901d5e5 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -36,8 +36,9 @@ export interface SavedSearch { columns?: Record; }; hideChart?: boolean; - sharingSavedObject?: { + sharingSavedObjectProps?: { outcome?: 'aliasMatch' | 'exactMatch' | 'conflict'; aliasTargetId?: string; + errorJSON?: string; }; } diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index 7c1aecd31c702..e633c45086b74 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -32,7 +32,7 @@ import { import { BaseVisType } from './vis_types'; import { VisParams } from '../common/types'; -import { getSavedSearch } from '../../discover/public'; +import { getSavedSearch, throwErrorOnSavedSearchUrlConflict } from '../../discover/public'; export interface SerializedVisData { expression?: string; @@ -66,6 +66,8 @@ const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: savedObjectsClient: getSavedObjects().client, }); + await throwErrorOnSavedSearchUrlConflict(savedSearch); + if (savedSearch?.searchSource) { inputSearchSource.setParent(savedSearch.searchSource); } diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index 9ac86469b0da3..9f6c21a24a361 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -16,7 +16,11 @@ import { import { SearchSourceFields } from 'src/plugins/data/public'; import { cloneDeep } from 'lodash'; import { ExpressionValueError } from 'src/plugins/expressions/public'; -import { getSavedSearch, SavedSearch } from '../../../../discover/public'; +import { + getSavedSearch, + SavedSearch, + throwErrorOnSavedSearchUrlConflict, +} from '../../../../discover/public'; import { SavedFieldNotFound, SavedFieldTypeInvalidForAgg } from '../../../../kibana_utils/common'; import { VisualizeServices } from '../types'; @@ -55,6 +59,8 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( search: data.search, savedObjectsClient: savedObjects.client, }); + + await throwErrorOnSavedSearchUrlConflict(savedSearch); } return { savedSearch, embeddableHandler }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index 547527cdf2d33..bd225ae9d0a32 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -11,11 +11,7 @@ import { i18n } from '@kbn/i18n'; import { isIndexPattern } from '../../../../common/types/index_pattern'; -import { - getSavedSearch, - getSavedSearchUrlConflictMessage, - savedSearchHasUrlConflict, -} from '../../../shared_imports'; +import { getSavedSearch, getSavedSearchUrlConflictMessage } from '../../../shared_imports'; import { useAppDependencies } from '../../app_dependencies'; @@ -54,10 +50,11 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { fetchedSavedSearch = await getSavedSearch(id, { search: appDeps.data.search, savedObjectsClient: appDeps.savedObjects.client, + spaces: appDeps.spaces, }); - if (savedSearchHasUrlConflict(fetchedSavedSearch)) { - setError(await getSavedSearchUrlConflictMessage(fetchedSavedSearch, appDeps.spaces)); + if (fetchedSavedSearch?.sharingSavedObjectProps?.errorJSON) { + setError(await getSavedSearchUrlConflictMessage(fetchedSavedSearch)); return; } } catch (e) { diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index 30d6412ab53df..b8f5d88205858 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -10,7 +10,6 @@ export { UseRequestConfig, useRequest } from '../../../../src/plugins/es_ui_shar export { getSavedSearch, getSavedSearchUrlConflictMessage, - savedSearchHasUrlConflict, } from '../../../../src/plugins/discover/public'; export { From 85cfdb130d138b4040f740bee939d777ecfd1103 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 6 Oct 2021 15:02:05 +0300 Subject: [PATCH 14/18] add spaces into visualize, visualizations --- .../saved_searches/get_saved_searches.ts | 2 +- src/plugins/visualizations/kibana.json | 2 +- src/plugins/visualizations/public/plugin.ts | 11 ++++++- src/plugins/visualizations/public/services.ts | 29 +++++++++++++++---- src/plugins/visualizations/public/vis.ts | 3 +- src/plugins/visualizations/tsconfig.json | 1 + src/plugins/visualize/kibana.json | 3 +- .../visualize/public/application/types.ts | 3 +- .../utils/get_visualization_instance.test.ts | 1 + .../utils/get_visualization_instance.ts | 3 +- src/plugins/visualize/public/plugin.ts | 3 ++ src/plugins/visualize/tsconfig.json | 3 +- 12 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/plugins/discover/public/saved_searches/get_saved_searches.ts b/src/plugins/discover/public/saved_searches/get_saved_searches.ts index e1e04d0a5ee25..32c50f691fe42 100644 --- a/src/plugins/discover/public/saved_searches/get_saved_searches.ts +++ b/src/plugins/discover/public/saved_searches/get_saved_searches.ts @@ -67,7 +67,7 @@ const findSavedSearch = async ( ? JSON.stringify({ targetType: SAVED_SEARCH_TYPE, sourceId: savedSearchId, - targetSpace: (await spaces!.getActiveSpace()).id, + targetSpace: (await spaces.getActiveSpace()).id, }) : undefined, } diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index 0afbec24c7c3f..f00506f5968af 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -11,7 +11,7 @@ "inspector", "savedObjects" ], - "optionalPlugins": ["usageCollection"], + "optionalPlugins": ["usageCollection", "spaces"], "requiredBundles": ["kibanaUtils", "discover"], "extraPublicDirs": ["common/constants", "common/prepare_log_table", "common/expression_functions"], "owner": { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index d8002a60256d7..82ea5939d2826 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -23,6 +23,7 @@ import { setOverlays, setEmbeddable, setDocLinks, + setSpaces, } from './services'; import { VISUALIZE_EMBEDDABLE_TYPE, @@ -63,6 +64,8 @@ import type { import type { DataPublicPluginSetup, DataPublicPluginStart } from '../../../plugins/data/public'; import type { ExpressionsSetup, ExpressionsStart } from '../../expressions/public'; import type { EmbeddableSetup, EmbeddableStart } from '../../embeddable/public'; +import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; + import { createVisAsync } from './vis_async'; /** @@ -100,6 +103,7 @@ export interface VisualizationsStartDeps { getAttributeService: EmbeddableStart['getAttributeService']; savedObjects: SavedObjectsStart; savedObjectsClient: SavedObjectsClientContract; + spaces?: SpacesPluginStart; } /** @@ -146,7 +150,7 @@ export class VisualizationsPlugin public start( core: CoreStart, - { data, expressions, uiActions, embeddable, savedObjects }: VisualizationsStartDeps + { data, expressions, uiActions, embeddable, savedObjects, spaces }: VisualizationsStartDeps ): VisualizationsStart { const types = this.types.start(); setTypes(types); @@ -163,6 +167,11 @@ export class VisualizationsPlugin setAggs(data.search.aggs); setOverlays(core.overlays); setChrome(core.chrome); + + if (spaces) { + setSpaces(spaces); + } + const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index f6b8145b3e352..64e1804fae184 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -18,12 +18,14 @@ import type { } from '../../../core/public'; import type { TypesStart } from './vis_types'; import { createGetterSetter } from '../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart, TimefilterContract } from '../../../plugins/data/public'; -import { UsageCollectionSetup } from '../../../plugins/usage_collection/public'; -import { ExpressionsStart } from '../../../plugins/expressions/public'; -import { UiActionsStart } from '../../../plugins/ui_actions/public'; -import { SavedVisualizationsLoader } from './saved_visualizations'; -import { EmbeddableStart } from '../../embeddable/public'; +import type { DataPublicPluginStart, TimefilterContract } from '../../../plugins/data/public'; +import type { UsageCollectionSetup } from '../../../plugins/usage_collection/public'; +import type { ExpressionsStart } from '../../../plugins/expressions/public'; +import type { UiActionsStart } from '../../../plugins/ui_actions/public'; +import type { SavedVisualizationsLoader } from './saved_visualizations'; +import type { EmbeddableStart } from '../../embeddable/public'; + +import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); @@ -62,3 +64,18 @@ export const [getAggs, setAggs] = export const [getOverlays, setOverlays] = createGetterSetter('Overlays'); export const [getChrome, setChrome] = createGetterSetter('Chrome'); + +const [_getSpaces, setSpaces] = createGetterSetter('Spaces'); + +/** "spaces" is optional, in case if that plugin is turned off "_getSpaces" method trigger the exception **/ +const getSpaces = () => { + let spaces: SpacesPluginStart | undefined; + try { + spaces = _getSpaces(); + } catch { + // nothing to be here + } + return spaces; +}; + +export { getSpaces, setSpaces }; diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index e633c45086b74..2a1e7f2c8c673 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -21,7 +21,7 @@ import { Assign } from '@kbn/utility-types'; import { i18n } from '@kbn/i18n'; import { PersistedState } from './persisted_state'; -import { getTypes, getAggs, getSearch, getSavedObjects } from './services'; +import { getTypes, getAggs, getSearch, getSavedObjects, getSpaces } from './services'; import { IAggConfigs, IndexPattern, @@ -64,6 +64,7 @@ const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: const savedSearch = await getSavedSearch(savedSearchId, { search: getSearch(), savedObjectsClient: getSavedObjects().client, + spaces: getSpaces(), }); await throwErrorOnSavedSearchUrlConflict(savedSearch); diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index 65ab83d5e0bae..eeaed655c3e73 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -23,5 +23,6 @@ { "path": "../usage_collection/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../discover/tsconfig.json" }, + { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, ] } diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json index afa9e3ce055b2..bfb23bec2111c 100644 --- a/src/plugins/visualize/kibana.json +++ b/src/plugins/visualize/kibana.json @@ -17,7 +17,8 @@ "home", "share", "savedObjectsTaggingOss", - "usageCollection" + "usageCollection", + "spaces" ], "requiredBundles": [ "kibanaUtils", diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index be49895a94114..71f2c7df25493 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -8,7 +8,6 @@ import type { EventEmitter } from 'events'; import type { History } from 'history'; - import type { SerializableRecord } from '@kbn/utility-types'; import type { @@ -46,6 +45,7 @@ import type { DashboardStart } from '../../../dashboard/public'; import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; import type { UsageCollectionStart } from '../../../usage_collection/public'; import type { SavedSearch } from '../../../discover/public'; +import type { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/public'; import { PureVisState } from '../../common/types'; @@ -106,6 +106,7 @@ export interface VisualizeServices extends CoreStart { presentationUtil: PresentationUtilPluginStart; usageCollection?: UsageCollectionStart; getKibanaVersion: () => string; + spaces?: SpacesPluginStart; } export interface VisInstance { diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts index 337f9d5c10000..a99a119e0d6bb 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.test.ts @@ -27,6 +27,7 @@ jest.mock('../../../../discover/public', () => ({ title: 'savedSearchTitle', searchSource: {}, }), + throwErrorOnSavedSearchUrlConflict: jest.fn(), })); describe('getVisualizationInstance', () => { diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index 9f6c21a24a361..eb6e63427a9af 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -36,7 +36,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( vis: Vis, visualizeServices: VisualizeServices ) => { - const { data, createVisEmbeddableFromObject, savedObjects } = visualizeServices; + const { data, createVisEmbeddableFromObject, savedObjects, spaces } = visualizeServices; const embeddableHandler = (await createVisEmbeddableFromObject(vis, { id: '', timeRange: data.query.timefilter.timefilter.getTime(), @@ -58,6 +58,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( savedSearch = await getSavedSearch(vis.data.savedSearchId, { search: data.search, savedObjectsClient: savedObjects.client, + spaces, }); await throwErrorOnSavedSearchUrlConflict(savedSearch); diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index b128c09209743..b0addf2bedb96 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -44,6 +44,7 @@ import type { EmbeddableStart } from '../../embeddable/public'; import type { DashboardStart } from '../../dashboard/public'; import type { SavedObjectTaggingOssPluginStart } from '../../saved_objects_tagging_oss/public'; import type { UsageCollectionStart } from '../../usage_collection/public'; +import type { SpacesApi } from '../../../../x-pack/plugins/spaces/public'; import { setVisEditorsRegistry, setUISettings, setUsageCollector } from './services'; import { createVisEditorsRegistry, VisEditorsRegistry } from './vis_editors_registry'; @@ -61,6 +62,7 @@ export interface VisualizePluginStartDependencies { savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; presentationUtil: PresentationUtilPluginStart; usageCollection?: UsageCollectionStart; + spaces?: SpacesApi; } export interface VisualizePluginSetupDependencies { @@ -212,6 +214,7 @@ export class VisualizePlugin presentationUtil: pluginsStart.presentationUtil, usageCollection: pluginsStart.usageCollection, getKibanaVersion: () => this.initializerContext.env.packageInfo.version, + spaces: pluginsStart.spaces, }; params.element.classList.add('visAppWrapper'); diff --git a/src/plugins/visualize/tsconfig.json b/src/plugins/visualize/tsconfig.json index 3f1f7487085bf..9c1e3fd72ff8b 100644 --- a/src/plugins/visualize/tsconfig.json +++ b/src/plugins/visualize/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../kibana_react/tsconfig.json" }, { "path": "../home/tsconfig.json" }, { "path": "../presentation_util/tsconfig.json" }, - { "path": "../discover/tsconfig.json" } + { "path": "../discover/tsconfig.json" }, + { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, ] } From 17d55167390190db7f66483c42e130862b7a257e Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 6 Oct 2021 17:19:08 +0300 Subject: [PATCH 15/18] fix Tim's comment --- .../application/apps/main/utils/persist_saved_search.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts b/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts index d42f5595489a9..584fbe14cb59e 100644 --- a/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts +++ b/src/plugins/discover/public/application/apps/main/utils/persist_saved_search.ts @@ -54,7 +54,9 @@ export async function persistSavedSearch( try { const id = await saveSavedSearch(savedSearch, saveOptions, services.core.savedObjects.client); - onSuccess(id!); + if (id) { + onSuccess(id); + } return { id }; } catch (saveError) { onError(saveError, savedSearch); From 06c15de2368a3d3020f45a14ab35d0011907bc34 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Thu, 7 Oct 2021 14:44:48 +0300 Subject: [PATCH 16/18] pass false into createGetterSetter for getSpaces --- src/plugins/visualizations/public/services.ts | 15 +-------------- x-pack/plugins/transform/kibana.json | 2 +- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index 64e1804fae184..b5db56e61ebe4 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -65,17 +65,4 @@ export const [getOverlays, setOverlays] = createGetterSetter('Over export const [getChrome, setChrome] = createGetterSetter('Chrome'); -const [_getSpaces, setSpaces] = createGetterSetter('Spaces'); - -/** "spaces" is optional, in case if that plugin is turned off "_getSpaces" method trigger the exception **/ -const getSpaces = () => { - let spaces: SpacesPluginStart | undefined; - try { - spaces = _getSpaces(); - } catch { - // nothing to be here - } - return spaces; -}; - -export { getSpaces, setSpaces }; +export const [getSpaces, setSpaces] = createGetterSetter('Spaces', false); diff --git a/x-pack/plugins/transform/kibana.json b/x-pack/plugins/transform/kibana.json index 6ad9b1e8d424f..f47b018dd1d26 100644 --- a/x-pack/plugins/transform/kibana.json +++ b/x-pack/plugins/transform/kibana.json @@ -17,7 +17,7 @@ "optionalPlugins": [ "security", "usageCollection", - "spaces" + "spaces", "alerting" ], "configPath": ["xpack", "transform"], From ff973c57d8a8ad0549dbd0a0c64b5ddd45e181fb Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Mon, 11 Oct 2021 19:01:18 +0300 Subject: [PATCH 17/18] Fix comments --- src/plugins/discover/kibana.json | 2 +- .../components/top_nav/on_save_search.tsx | 1 + .../apps/main/discover_main_route.tsx | 35 +++++++++++-------- .../saved_searches/save_saved_searches.ts | 2 ++ .../saved_search_url_conflict_callout.ts | 1 + src/plugins/discover/tsconfig.json | 1 + .../visualize/public/application/types.ts | 1 - 7 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 09557ecddea6a..3d5fdefd276d3 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -16,7 +16,7 @@ "indexPatternFieldEditor" ], "optionalPlugins": ["home", "share", "usageCollection", "spaces"], - "requiredBundles": ["kibanaUtils", "home", "kibanaReact", "fieldFormats"], + "requiredBundles": ["kibanaUtils", "home", "kibanaReact", "fieldFormats", "dataViews"], "extraPublicDirs": ["common"], "owner": { "name": "Data Discovery", diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx index 9418783592bf6..18766b5df7f33 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx @@ -111,6 +111,7 @@ export async function onSaveSearch({ const saveOptions: SaveSavedSearchOptions = { onTitleDuplicate, copyOnSave: newCopyOnSave, + isTitleDuplicateConfirmed, }; const response = await saveDataSource({ indexPattern, diff --git a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx index 366c3d1ab04f6..b674bfd6568ac 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx @@ -19,6 +19,7 @@ import { loadIndexPattern, resolveIndexPattern } from './utils/resolve_index_pat import { DiscoverMainApp } from './discover_main_app'; import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../../helpers/breadcrumbs'; import { redirectWhenMissing } from '../../../../../kibana_utils/public'; +import { DataViewSavedObjectConflictError } from '../../../../../data_views/common'; import { getUrlTracker } from '../../../kibana_services'; import { LoadingIndicator } from '../../components/common/loading_indicator'; @@ -119,22 +120,26 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { ); } } catch (e) { - redirectWhenMissing({ - history, - navigateToApp: core.application.navigateToApp, - basePath, - mapping: { - search: '/', - 'index-pattern': { - app: 'management', - path: `kibana/objects/savedSearches/${id}`, + if (e instanceof DataViewSavedObjectConflictError) { + setError(e); + } else { + redirectWhenMissing({ + history, + navigateToApp: core.application.navigateToApp, + basePath, + mapping: { + search: '/', + 'index-pattern': { + app: 'management', + path: `kibana/objects/savedSearches/${id}`, + }, }, - }, - toastNotifications, - onBeforeRedirect() { - getUrlTracker().setTrackedUrl('/'); - }, - })(e); + toastNotifications, + onBeforeRedirect() { + getUrlTracker().setTrackedUrl('/'); + }, + })(e); + } } } diff --git a/src/plugins/discover/public/saved_searches/save_saved_searches.ts b/src/plugins/discover/public/saved_searches/save_saved_searches.ts index 34cc76bf7b0d6..c3440bdb7696a 100644 --- a/src/plugins/discover/public/saved_searches/save_saved_searches.ts +++ b/src/plugins/discover/public/saved_searches/save_saved_searches.ts @@ -13,6 +13,7 @@ import { toSavedSearchAttributes } from './saved_searches_utils'; export interface SaveSavedSearchOptions { onTitleDuplicate?: () => void; + isTitleDuplicateConfirmed?: boolean; copyOnSave?: boolean; } @@ -48,6 +49,7 @@ export const saveSavedSearch = async ( if (savedSearch.title) { if ( isNew && + !options.isTitleDuplicateConfirmed && options.onTitleDuplicate && (await hasDuplicatedTitle(savedSearch.title, savedObjectsClient)) ) { diff --git a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts index fd07126c496cf..dea370e6bd1e2 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts @@ -24,6 +24,7 @@ export const SavedSearchURLConflictCallout = ({ spaces, history, }: SavedSearchURLConflictCalloutProps) => { + console.log('SavedSearchURLConflictCallout', savedSearch) if (spaces && savedSearch?.id && savedSearch?.sharingSavedObjectProps?.outcome === 'conflict') { const otherObjectId = savedSearch.sharingSavedObjectProps?.aliasTargetId; diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 7a6736b30d9f3..eb739e673cacd 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -25,6 +25,7 @@ { "path": "../kibana_legacy/tsconfig.json" }, { "path": "../index_pattern_field_editor/tsconfig.json"}, { "path": "../field_formats/tsconfig.json" }, + { "path": "../data_views/tsconfig.json" }, { "path": "../../../x-pack/plugins/spaces/tsconfig.json" } ] } diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 5306567c5f34e..e77520c962d88 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -46,7 +46,6 @@ import type { DashboardStart } from '../../../dashboard/public'; import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; import type { UsageCollectionStart } from '../../../usage_collection/public'; import type { SavedSearch } from '../../../discover/public'; -import type { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/public'; import { PureVisState } from '../../common/types'; From 5bf1a0e7ed8bd8a7eba4a9737004c11b2847403e Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Mon, 11 Oct 2021 21:18:51 +0300 Subject: [PATCH 18/18] Fix lint --- .../public/saved_searches/saved_search_url_conflict_callout.ts | 1 - src/plugins/visualize/public/plugin.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts index dea370e6bd1e2..fd07126c496cf 100644 --- a/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts +++ b/src/plugins/discover/public/saved_searches/saved_search_url_conflict_callout.ts @@ -24,7 +24,6 @@ export const SavedSearchURLConflictCallout = ({ spaces, history, }: SavedSearchURLConflictCalloutProps) => { - console.log('SavedSearchURLConflictCallout', savedSearch) if (spaces && savedSearch?.id && savedSearch?.sharingSavedObjectProps?.outcome === 'conflict') { const otherObjectId = savedSearch.sharingSavedObjectProps?.aliasTargetId; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index de14832ad5f98..7ff3434286b6b 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -28,7 +28,6 @@ import { createKbnUrlStateStorage, withNotifyOnErrors, } from '../../kibana_utils/public'; -import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; import { VisualizeConstants } from './application/visualize_constants'; import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public';