[Discover] Removing SavedObject usage for savedSearch (#112983)

* [Discover] Step 2 - remove SavedObjectLoader

* Fix PR comments

* fix test names

* fix ts error

* add handling of missed 'so'

* add Embeddable error

* fix jest

* add DiscoverError component

* fix Joe comments

* add search params

* add throwErrorOnUrlConflict util method

* add error handling into transform plugin

* do some updates

* add spaces into visualize, visualizations

* fix Tim's comment

* pass false into createGetterSetter for getSpaces

* Fix comments

* Fix lint

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Uladzislau Lasitsa <Uladzislau_Lasitsa@epam.com>
This commit is contained in:
Alexey Antonov 2021-10-13 11:31:04 +03:00 committed by GitHub
parent 62d39cc1fd
commit d9ef453b26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 1318 additions and 282 deletions

View file

@ -15,8 +15,8 @@
"savedObjects",
"indexPatternFieldEditor"
],
"optionalPlugins": ["home", "share", "usageCollection"],
"requiredBundles": ["kibanaUtils", "home", "kibanaReact", "fieldFormats"],
"optionalPlugins": ["home", "share", "usageCollection", "spaces"],
"requiredBundles": ["kibanaUtils", "home", "kibanaReact", "fieldFormats", "dataViews"],
"extraPublicDirs": ["common"],
"owner": {
"name": "Data Discovery",

View file

@ -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;

View file

@ -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 },
},

View file

@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Filter, IndexPattern, SearchSource } from 'src/plugins/data/public';
import { Filter, IndexPattern, 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';
@ -53,7 +53,7 @@ export async function fetchSurroundingDocs(
}
const { data } = getServices();
const timeField = indexPattern.timeFieldName!;
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);
@ -104,7 +104,7 @@ export async function fetchSurroundingDocs(
}
export function updateSearchSource(
searchSource: SearchSource,
searchSource: ISearchSource,
indexPattern: IndexPattern,
filters: Filter[],
useNewFieldsApi: boolean

View file

@ -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}

View file

@ -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
@ -65,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<ElasticSearchHit | undefined>(undefined);
const [inspectorSession, setInspectorSession] = useState<InspectorSession | undefined>(undefined);
const fetchCounter = useRef<number>(0);
@ -80,6 +92,8 @@ export function DiscoverLayout({
}
}, [dataState.fetchStatus]);
useSavedSearchAliasMatchRedirect({ savedSearch, spaces, history });
const timeField = useMemo(() => {
return indexPattern.type !== 'rollup' ? indexPattern.timeFieldName : undefined;
}, [indexPattern]);
@ -174,6 +188,11 @@ export function DiscoverLayout({
resetSavedSearch={resetSavedSearch}
/>
<EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle">
<SavedSearchURLConflictCallout
savedSearch={savedSearch}
spaces={spaces}
history={history}
/>
<h1 id="savedSearchTitle" className="euiScreenReaderOnly">
{savedSearch.title}
</h1>

View file

@ -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,10 @@ 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,
isTitleDuplicateConfirmed,
};
const response = await saveDataSource({
indexPattern,
@ -133,7 +134,7 @@ export async function onSaveSearch({
<SavedObjectSaveModal
onSave={onSave}
onClose={() => {}}
title={savedSearch.title}
title={savedSearch.title ?? ''}
showCopyOnSave={!!savedSearch.id}
objectType={i18n.translate('discover.localMenu.saveSaveSearchObjectType', {
defaultMessage: 'search',

View file

@ -8,15 +8,18 @@
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 { 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 } 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';
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';
@ -37,6 +40,21 @@ interface DiscoverLandingParams {
id: string;
}
const DiscoverError = ({ error }: { error: Error }) => (
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
title={
<h2>
{i18n.translate('discover.discoverError.title', {
defaultMessage: 'Error loading Discover',
})}
</h2>
}
body={<p>{error.message}</p>}
/>
);
export function DiscoverMainRoute({ services, history }: DiscoverMainProps) {
const {
core,
@ -46,7 +64,7 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) {
toastNotifications,
http: { basePath },
} = services;
const [error, setError] = useState<Error>();
const [savedSearch, setSavedSearch] = useState<SavedSearch>();
const indexPattern = savedSearch?.searchSource?.getField('index');
const [indexPatternList, setIndexPatternList] = useState<
@ -58,58 +76,76 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) {
useEffect(() => {
const savedSearchId = id;
async function loadDefaultOrCurrentIndexPattern(usedSavedSearch: SavedSearch) {
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<SavedObject<IndexPatternAttributes>>;
const indexPatternData = await resolveIndexPattern(
ip,
usedSavedSearch.searchSource,
toastNotifications
);
setIndexPatternList(ipList);
return indexPatternData;
async function loadDefaultOrCurrentIndexPattern(searchSource: ISearchSource) {
try {
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<SavedObject<IndexPatternAttributes>>;
const indexPatternData = await resolveIndexPattern(ip, searchSource, toastNotifications);
setIndexPatternList(ipList);
return indexPatternData;
} catch (e) {
setError(e);
}
}
async function loadSavedSearch() {
try {
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,
spaces: services.spaces,
});
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) {
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);
}
}
}
loadSavedSearch();
}, [
core.savedObjects.client,
basePath,
chrome.recentlyAccessed,
config,
@ -129,6 +165,10 @@ export function DiscoverMainRoute({ services, history }: DiscoverMainProps) {
);
}, [chrome, savedSearch]);
if (error) {
return <DiscoverError error={error} />;
}
if (!indexPattern || !savedSearch) {
return <LoadingIndicator />;
}

View file

@ -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;

View file

@ -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();

View file

@ -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 {
@ -148,7 +148,12 @@ export function useDiscoverState({
*/
const resetSavedSearch = useCallback(
async (id?: string) => {
const newSavedSearch = await services.getSavedSearchById(id);
const newSavedSearch = await getSavedSearch(id, {
search: services.data.search,
savedObjectsClient: services.core.savedObjects.client,
spaces: services.spaces,
});
const newIndexPattern = newSavedSearch.searchSource.getField('index') || indexPattern;
newSavedSearch.searchSource.setField('index', newIndexPattern);
const newAppState = getStateDefaults({

View file

@ -9,7 +9,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
import { BehaviorSubject, Subject } from 'rxjs';
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;

View file

@ -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;

View file

@ -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
) {

View file

@ -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,

View file

@ -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,

View file

@ -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();

View file

@ -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
) {

View file

@ -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 = () => {

View file

@ -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';
@ -33,7 +33,7 @@ export function getFetch$({
main$: DataMain$;
refetch$: DataRefetch$;
searchSessionManager: DiscoverSearchSessionManager;
searchSource: SearchSource;
searchSource: ISearchSource;
initialFetchStatus: FetchStatus;
}) {
const { timefilter } = data.query.timefilter;

View file

@ -11,7 +11,7 @@ import type { IUiSettingsClient } from 'src/core/public';
import type { DataPublicPluginStart } from 'src/plugins/data/public';
import type { ISearchSource, SearchSourceFields } 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';

View file

@ -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,

View file

@ -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,10 @@ export async function persistSavedSearch(
}
try {
const id = await savedSearch.save(saveOptions);
onSuccess(id);
const id = await saveSavedSearch(savedSearch, saveOptions, services.core.savedObjects.client);
if (id) {
onSuccess(id);
}
return { id };
} catch (saveError) {
onError(saveError, savedSearch);

View file

@ -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;

View file

@ -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', () => {

View file

@ -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';

View file

@ -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 = {

View file

@ -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;
}

View file

@ -20,6 +20,11 @@ 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,
throwErrorOnSavedSearchUrlConflict,
} from '../../saved_searches';
interface StartServices {
executeTriggerActions: UiActionsStart['executeTriggerActions'];
@ -59,20 +64,27 @@ export class SearchEmbeddableFactory
input: Partial<SearchInput> & { id: string; timeRange: TimeRange },
parent?: Container
): Promise<SavedSearchEmbeddable | ErrorEmbeddable> => {
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,
spaces: services.spaces,
});
await throwErrorOnSavedSearchUrlConflict(savedSearch);
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,

View file

@ -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 {

View file

@ -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<ViewSearchContext> {
async execute(context: ActionExecutionContext<ViewSearchContext>): Promise<void> {
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 });
}

View file

@ -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<SavedSearch>;
getSavedSearchUrlById: (id: string) => Promise<string>;
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,
};
}

View file

@ -9,12 +9,23 @@
import { PluginInitializerContext } from 'kibana/public';
import { DiscoverPlugin } from './plugin';
export {
getSavedSearch,
getSavedSearchFullPathUrl,
getSavedSearchUrl,
getSavedSearchUrlConflictMessage,
throwErrorOnSavedSearchUrlConflict,
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';

View file

@ -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'],

View file

@ -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,
}),
},
};
}

View file

@ -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';

View file

@ -0,0 +1,144 @@
/*
* 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 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: {
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],
},
"sharingSavedObjectProps": Object {
"aliasTargetId": undefined,
"errorJSON": undefined,
"outcome": "exactMatch",
},
"sort": Array [
Array [
"order_date",
"desc",
],
],
"title": "test1",
}
`);
});
});

View file

@ -0,0 +1,85 @@
/*
* 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';
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 = ({
search,
}: {
search: DataPublicPluginStart['search'];
}): SavedSearch => ({
searchSource: search.searchSource.createEmpty(),
});
const findSavedSearch = async (
savedSearchId: string,
{ search, savedObjectsClient, spaces }: GetSavedSearchDependencies
) => {
const so = await savedObjectsClient.resolve<SavedSearchAttributes>(
SAVED_SEARCH_TYPE,
savedSearchId
);
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 ?? '{}'
);
const searchSourceValues = injectSearchSourceReferences(
parsedSearchSourceJSON as Parameters<typeof injectSearchSourceReferences>[0],
savedSearch.references
);
return fromSavedSearchAttributes(
savedSearchId,
savedSearch.attributes,
await search.searchSource.create(searchSourceValues),
{
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,
}
);
};
/** @public **/
export const getSavedSearch = async (
savedSearchId: string | undefined,
dependencies: GetSavedSearchDependencies
) => {
return savedSearchId
? findSavedSearch(savedSearchId, dependencies)
: getEmptySavedSearch(dependencies);
};

View file

@ -6,5 +6,25 @@
* 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,
getSavedSearchUrlConflictMessage,
throwErrorOnSavedSearchUrlConflict,
} 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';
/** @deprecated __LEGACY object will be removed in v8**/
export const __LEGACY = {
createSavedSearchesLoader,
};

View file

@ -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));
}
}

View file

@ -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';

View file

@ -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;
}

View file

@ -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<string>;
copyOnSave?: boolean;
hideChart?: boolean;
}
/** @deprecated **/
export interface SavedSearchLoader {
get: (id: string) => Promise<LegacySavedSearch>;
urlFor: (id: string) => string;
}

View file

@ -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: [],
}),
},
sharingSavedObjectProps: {
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: [] }
);
});
});

View file

@ -0,0 +1,80 @@
/*
* 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;
isTitleDuplicateConfirmed?: boolean;
copyOnSave?: boolean;
}
const hasDuplicatedTitle = async (
title: string,
savedObjectsClient: SavedObjectsStart['client']
): Promise<boolean | void> => {
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<string | undefined> => {
const isNew = options.copyOnSave || !savedSearch.id;
if (savedSearch.title) {
if (
isNew &&
!options.isTitleDuplicateConfirmed &&
options.onTitleDuplicate &&
(await hasDuplicatedTitle(savedSearch.title, savedObjectsClient))
) {
options.onTitleDuplicate();
return;
}
}
const { searchSourceJSON, references } = savedSearch.searchSource.serialize();
const resp = isNew
? await savedObjectsClient.create<SavedSearchAttributes>(
SAVED_SEARCH_TYPE,
toSavedSearchAttributes(savedSearch, searchSourceJSON),
{
references,
}
)
: await savedObjectsClient.update<SavedSearchAttributes>(
SAVED_SEARCH_TYPE,
savedSearch.id!,
toSavedSearchAttributes(savedSearch, searchSourceJSON),
{
references,
}
);
return resp?.id;
};

View file

@ -0,0 +1,73 @@
/*
* 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 type { History } from 'history';
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<typeof spacesPluginMock.createStartContract>;
let history: () => History;
beforeEach(() => {
spaces = spacesPluginMock.createStartContract();
history = () =>
({
location: {
search: '?_g=foo',
},
} as History);
});
test('should redirect in case of aliasMatch', () => {
const savedSearch = {
id: 'id',
sharingSavedObjectProps: {
outcome: 'aliasMatch',
aliasTargetId: 'aliasTargetId',
},
} as SavedSearch;
renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch, history }));
expect(spaces.ui.redirectLegacyUrl).toHaveBeenCalledWith(
'#/view/aliasTargetId?_g=foo',
' search'
);
});
test('should not redirect if outcome !== aliasMatch', () => {
const savedSearch = {
id: 'id',
sharingSavedObjectProps: {
outcome: 'exactMatch',
},
} as SavedSearch;
renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch, history }));
expect(spaces.ui.redirectLegacyUrl).not.toHaveBeenCalled();
});
test('should not redirect if aliasTargetId is not defined', () => {
const savedSearch = {
id: 'id',
sharingSavedObjectProps: {
outcome: 'aliasMatch',
},
} as SavedSearch;
renderHook(() => useSavedSearchAliasMatchRedirect({ spaces, savedSearch, history }));
expect(spaces.ui.redirectLegacyUrl).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,49 @@
/*
* 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 { History } from 'history';
import { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { getSavedSearchUrl } from './saved_searches_utils';
import type { SavedSearch } from './types';
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() {
if (savedSearch) {
const { aliasTargetId, outcome } = savedSearch.sharingSavedObjectProps ?? {};
if (spaces && aliasTargetId && outcome === 'aliasMatch') {
await spaces.ui.redirectLegacyUrl(
`${getSavedSearchUrl(aliasTargetId)}${history().location.search}`,
i18n.translate('discover.savedSearchAliasMatchRedirect.objectNoun', {
defaultMessage: '{savedSearch} search',
values: {
savedSearch: savedSearch.title,
},
})
);
}
}
}
aliasMatchRedirect();
}, [savedSearch, spaces, history]);
};

View file

@ -0,0 +1,61 @@
/*
* 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 type { History } from 'history';
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<typeof spacesPluginMock.createStartContract>;
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", () => {
const savedSearch = {
id: 'id',
sharingSavedObjectProps: {
outcome: 'conflict',
aliasTargetId: 'aliasTargetId',
},
} as SavedSearch;
const component = mountWithIntl(
<SavedSearchURLConflictCallout spaces={spaces} savedSearch={savedSearch} history={history} />
);
expect(component.children()).toMatchInlineSnapshot(`"callout"`);
});
test('should not render URLConflictCallout in case of no conflicts', () => {
const savedSearch = {
id: 'id',
sharingSavedObjectProps: {},
} as SavedSearch;
const component = mountWithIntl(
<SavedSearchURLConflictCallout spaces={spaces} savedSearch={savedSearch} history={history} />
);
expect(component.children()).toMatchInlineSnapshot(`null`);
});
});

View file

@ -0,0 +1,46 @@
/*
* 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 type { History } from 'history';
import { getSavedSearchUrl } from './saved_searches_utils';
import type { SavedSearch } from './types';
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 && savedSearch?.sharingSavedObjectProps?.outcome === 'conflict') {
const otherObjectId = savedSearch.sharingSavedObjectProps?.aliasTargetId;
if (otherObjectId) {
return spaces.ui.components.getLegacyUrlConflict({
objectNoun: i18n.translate('discover.savedSearchURLConflictCallout.objectNoun', {
defaultMessage: '{savedSearch} search',
values: {
savedSearch: savedSearch.title,
},
}),
currentObjectId: savedSearch.id,
otherObjectPath: `${getSavedSearchUrl(otherObjectId)}${history().location.search}`,
otherObjectId,
});
}
}
return null;
};

View file

@ -0,0 +1,140 @@
/*
* 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,
throwErrorOnSavedSearchUrlConflict,
} 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,
},
"sharingSavedObjectProps": Object {},
"sort": Array [],
"title": "saved search",
}
`);
});
});
describe('throwErrorOnSavedSearchUrlConflict', () => {
test('should throw an error on url conflict', async () => {
let error = 'no error';
try {
await throwErrorOnSavedSearchUrlConflict({
id: 'id',
sharingSavedObjectProps: {
outcome: 'conflict',
errorJSON: '{}',
},
} 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 : {}'
);
});
});
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",
}
`);
});
});
});

View file

@ -0,0 +1,57 @@
/*
* 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 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 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: savedSearch.sharingSavedObjectProps?.errorJSON,
},
});
export const throwErrorOnSavedSearchUrlConflict = async (savedSearch: SavedSearch) => {
if (savedSearch.sharingSavedObjectProps?.errorJSON) {
throw new Error(await getSavedSearchUrlConflictMessage(savedSearch));
}
};
export const fromSavedSearchAttributes = (
id: string,
attributes: SavedSearchAttributes,
searchSource: SavedSearch['searchSource'],
sharingSavedObjectProps: SavedSearch['sharingSavedObjectProps']
): SavedSearch => ({
id,
searchSource,
sharingSavedObjectProps,
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,
});

View file

@ -6,26 +6,39 @@
* 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<string>;
lastSavedTitle?: string;
copyOnSave?: boolean;
description: string;
grid: {
columns?: Record<string, DiscoverGridSettingsColumn>;
};
hideChart: boolean;
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
}
/** @public **/
export interface SavedSearch {
searchSource: ISearchSource;
id?: string;
title?: string;
sort?: Array<[string, string]>;
columns?: string[];
description?: string;
grid?: {
columns?: Record<string, DiscoverGridSettingsColumn>;
};
hideChart?: boolean;
}
export interface SavedSearchLoader {
get: (id: string) => Promise<SavedSearch>;
urlFor: (id: string) => string;
sharingSavedObjectProps?: {
outcome?: 'aliasMatch' | 'exactMatch' | 'conflict';
aliasTargetId?: string;
errorJSON?: string;
};
}

View file

@ -24,6 +24,8 @@
{ "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": "../data_views/tsconfig.json" },
{ "path": "../../../x-pack/plugins/spaces/tsconfig.json" }
]
}

View file

@ -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"

View file

@ -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;
}

View file

@ -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]);

View file

@ -23,9 +23,9 @@ import {
setAggs,
setChrome,
setOverlays,
setSavedSearchLoader,
setEmbeddable,
setDocLinks,
setSpaces,
} from './services';
import {
VISUALIZE_EMBEDDABLE_TYPE,
@ -51,8 +51,6 @@ import {
findListItems,
} from './utils/saved_visualize_utils';
import { createSavedSearchesLoader } from '../../discover/public';
import type {
PluginInitializerContext,
CoreSetup,
@ -191,6 +189,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,
@ -198,11 +201,7 @@ export class VisualizationsPlugin
visualizationTypes: types,
});
setSavedVisualizationsLoader(savedVisualizationsLoader);
const savedSearchLoader = createSavedSearchesLoader({
savedObjectsClient: core.savedObjects.client,
savedObjects,
});
setSavedSearchLoader(savedSearchLoader);
return {
...types,
showNewVisModal,

View file

@ -16,8 +16,8 @@
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 '../utils/saved_visualization_references';
import { createSavedSearchesLoader } from '../../../discover/public';
import type { SavedObjectsClientContract } from '../../../../core/public';
import type { IndexPatternsContract } from '../../../../plugins/data/public';
import type { ISavedVis } from '../types';
@ -30,7 +30,7 @@ export interface SavedVisServices {
/** @deprecated **/
export function createSavedVisClass(services: SavedVisServices) {
const savedSearch = createSavedSearchesLoader(services);
const savedSearch = __LEGACY.createSavedSearchesLoader(services);
class SavedVis extends services.savedObjects.SavedObjectClass {
public static type: string = 'visualization';

View file

@ -18,13 +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 { SavedObjectLoader } from '../../saved_objects/public';
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<IUiSettingsClient>('UISettings');
@ -64,5 +65,4 @@ export const [getOverlays, setOverlays] = createGetterSetter<OverlayStart>('Over
export const [getChrome, setChrome] = createGetterSetter<ChromeStart>('Chrome');
export const [getSavedSearchLoader, setSavedSearchLoader] =
createGetterSetter<SavedObjectLoader>('savedSearchLoader');
export const [getSpaces, setSpaces] = createGetterSetter<SpacesPluginStart>('Spaces', false);

View file

@ -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, getSpaces } 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, throwErrorOnSavedSearchUrlConflict } from '../../discover/public';
export interface SerializedVisData {
expression?: string;
aggs: AggConfigSerialized[];
@ -58,14 +60,20 @@ export interface VisData {
}
const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: string) => {
const searchSource = inputSearchSource.createCopy();
if (savedSearchId) {
const savedSearch = await getSavedSearchLoader().get(savedSearchId);
const savedSearch = await getSavedSearch(savedSearchId, {
search: getSearch(),
savedObjectsClient: getSavedObjects().client,
spaces: getSpaces(),
});
searchSource.setParent(savedSearch.searchSource);
await throwErrorOnSavedSearchUrlConflict(savedSearch);
if (savedSearch?.searchSource) {
inputSearchSource.setParent(savedSearch.searchSource);
}
}
searchSource.setField('size', 0);
return searchSource;
return inputSearchSource;
};
type PartialVisState = Assign<SerializedVis, { data: Partial<SerializedVisData> }>;

View file

@ -8,7 +8,6 @@
import type { EventEmitter } from 'events';
import type { History } from 'history';
import type { SerializableRecord } from '@kbn/utility-types';
import type {
@ -38,7 +37,7 @@ 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';
@ -46,6 +45,7 @@ import type { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/pub
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';
@ -108,20 +108,15 @@ export interface VisualizeServices extends CoreStart {
spaces?: SpacesPluginStart;
}
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<TVisParams = VisParams> = new (
@ -142,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

View file

@ -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,13 @@ 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: {},
}),
throwErrorOnSavedSearchUrlConflict: jest.fn(),
}));
let savedVisMock: VisSavedObject;
@ -116,9 +116,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 () => {

View file

@ -14,10 +14,13 @@ 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,
throwErrorOnSavedSearchUrlConflict,
} from '../../../../discover/public';
import { SavedFieldNotFound, SavedFieldTypeInvalidForAgg } from '../../../../kibana_utils/common';
import { VisualizeServices } from '../types';
@ -33,8 +36,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async (
vis: Vis,
visualizeServices: VisualizeServices
) => {
const { data, createVisEmbeddableFromObject, savedObjects, savedObjectsPublic } =
visualizeServices;
const { data, createVisEmbeddableFromObject, savedObjects, spaces } = visualizeServices;
const embeddableHandler = (await createVisEmbeddableFromObject(vis, {
id: '',
timeRange: data.query.timefilter.timefilter.getTime(),
@ -50,13 +52,16 @@ 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);
spaces,
});
await throwErrorOnSavedSearchUrlConflict(savedSearch);
}
return { savedSearch, embeddableHandler };

View file

@ -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';
@ -45,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';
@ -62,7 +62,7 @@ export interface VisualizePluginStartDependencies {
savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart;
presentationUtil: PresentationUtilPluginStart;
usageCollection?: UsageCollectionStart;
spaces: SpacesPluginStart;
spaces?: SpacesApi;
}
export interface VisualizePluginSetupDependencies {

View file

@ -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',
});
});

View file

@ -122,7 +122,7 @@ export class ReportingCsvPanelAction implements ActionDefinition<ActionContext>
const immediateJobParams = this.apiClient.getDecoratedJobParams({
searchSource: getSearchSource(true),
columns,
title: savedSearch.title,
title: savedSearch.title || '',
objectType: 'downloadCsv', // FIXME: added for typescript, but immediate download job does not need objectType
});

View file

@ -17,6 +17,7 @@
"optionalPlugins": [
"security",
"usageCollection",
"spaces",
"alerting"
],
"configPath": ["xpack", "transform"],

View file

@ -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';

View file

@ -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 = () => {

View file

@ -20,12 +20,10 @@ import { isIndexPattern } from '../../../../common/types/index_pattern';
export type SavedSearchQuery = object;
type IndexPatternId = string;
type SavedSearchId = string;
let indexPatternCache: Array<SimpleSavedObject<Record<string, any>>> = [];
let fullIndexPatterns;
let currentIndexPattern = null;
let currentSavedSearch = null;
export let refreshIndexPatterns: () => Promise<unknown>;
@ -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;

View file

@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
import { isIndexPattern } from '../../../../common/types/index_pattern';
import { createSavedSearchesLoader } from '../../../shared_imports';
import { getSavedSearch, getSavedSearchUrlConflictMessage } 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<SearchItems | undefined>(undefined);
@ -52,7 +47,16 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => {
}
try {
fetchedSavedSearch = await loadCurrentSavedSearch(savedSearches, id);
fetchedSavedSearch = await getSavedSearch(id, {
search: appDeps.data.search,
savedObjectsClient: appDeps.savedObjects.client,
spaces: appDeps.spaces,
});
if (fetchedSavedSearch?.sharingSavedObjectProps?.errorJSON) {
setError(await getSavedSearchUrlConflictMessage(fetchedSavedSearch));
return;
}
} catch (e) {
// Just let fetchedSavedSearch stay undefined in case it doesn't exist.
}

View file

@ -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(),
};

View file

@ -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';
import type { PluginSetupContract as AlertingSetup } from '../../alerting/public';
import type { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public';
@ -24,6 +25,7 @@ export interface PluginsDependencies {
home: HomePublicPluginSetup;
savedObjects: SavedObjectsStart;
share: SharePluginStart;
spaces?: SpacesApi;
alerting?: AlertingSetup;
triggersActionsUi?: TriggersAndActionsUIPublicPluginSetup;
}

View file

@ -5,9 +5,12 @@
* 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,
getSavedSearchUrlConflictMessage,
} from '../../../../src/plugins/discover/public';
export {
getMlSharedImports,