Move search source parsing and serializing to data (#59919) (#63104)

This commit is contained in:
Joe Reuter 2020-04-09 15:52:55 +02:00 committed by GitHub
parent 81de77cacd
commit 3078bfa788
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 688 additions and 276 deletions

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [createSearchSource](./kibana-plugin-plugins-data-public.createsearchsource.md)
## createSearchSource variable
Deserializes a json string and a set of referenced objects to a `SearchSource` instance. Use this method to re-create the search source serialized using `searchSource.serialize`<!-- -->.
This function is a factory function that returns the actual utility when calling it with the required service dependency (index patterns contract). A pre-wired version is also exposed in the start contract of the data plugin as part of the search service
<b>Signature:</b>
```typescript
createSearchSource: (indexPatterns: Pick<import("../../index_patterns/index_patterns").IndexPatternsService, "get" | "clearCache" | "getFieldsForTimePattern" | "getFieldsForWildcard" | "getIds" | "getTitles" | "getFields" | "getCache" | "getDefault" | "make">) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise<SearchSource>
```

View file

@ -102,6 +102,7 @@
| [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-public.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string |
| [connectToQueryState](./kibana-plugin-plugins-data-public.connecttoquerystate.md) | Helper to setup two-way syncing of global data and a state container |
| [createSavedQueryService](./kibana-plugin-plugins-data-public.createsavedqueryservice.md) | |
| [createSearchSource](./kibana-plugin-plugins-data-public.createsearchsource.md) | Deserializes a json string and a set of referenced objects to a <code>SearchSource</code> instance. Use this method to re-create the search source serialized using <code>searchSource.serialize</code>.<!-- -->This function is a factory function that returns the actual utility when calling it with the required service dependency (index patterns contract). A pre-wired version is also exposed in the start contract of the data plugin as part of the search service |
| [ES\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.es_search_strategy.md) | |
| [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | |
| [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | |

View file

@ -38,6 +38,7 @@ export declare class SearchSource
| [getParent()](./kibana-plugin-plugins-data-public.searchsource.getparent.md) | | Get the parent of this SearchSource {<!-- -->undefined\|searchSource<!-- -->} |
| [getSearchRequestBody()](./kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md) | | |
| [onRequestStart(handler)](./kibana-plugin-plugins-data-public.searchsource.onrequeststart.md) | | Add a handler that will be notified whenever requests start |
| [serialize()](./kibana-plugin-plugins-data-public.searchsource.serialize.md) | | Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object.<!-- -->The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named <code>kibanaSavedObjectMeta.searchSourceJSON.index</code> and <code>kibanaSavedObjectMeta.searchSourceJSON.filter[&lt;number&gt;].meta.index</code>.<!-- -->Using <code>createSearchSource</code>, the instance can be re-created. |
| [setField(field, value)](./kibana-plugin-plugins-data-public.searchsource.setfield.md) | | |
| [setFields(newFields)](./kibana-plugin-plugins-data-public.searchsource.setfields.md) | | |
| [setParent(parent, options)](./kibana-plugin-plugins-data-public.searchsource.setparent.md) | | Set a searchSource that this source should inherit from |

View file

@ -0,0 +1,27 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) &gt; [serialize](./kibana-plugin-plugins-data-public.searchsource.serialize.md)
## SearchSource.serialize() method
Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object.
The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index` and `kibanaSavedObjectMeta.searchSourceJSON.filter[<number>].meta.index`<!-- -->.
Using `createSearchSource`<!-- -->, the instance can be re-created.
<b>Signature:</b>
```typescript
serialize(): {
searchSourceJSON: string;
references: SavedObjectReference[];
};
```
<b>Returns:</b>
`{
searchSourceJSON: string;
references: SavedObjectReference[];
}`

View file

@ -72,6 +72,7 @@ export async function buildServices(
const services = {
savedObjectsClient: core.savedObjects.client,
indexPatterns: plugins.data.indexPatterns,
search: plugins.data.search,
chrome: core.chrome,
overlays: core.overlays,
};

View file

@ -56,6 +56,7 @@ export const savedObjectManagementRegistry: ISavedObjectsManagementRegistry = {
const services = {
savedObjectsClient: npStart.core.savedObjects.client,
indexPatterns: npStart.plugins.data.indexPatterns,
search: npStart.plugins.data.search,
chrome: npStart.core.chrome,
overlays: npStart.core.overlays,
};

View file

@ -519,7 +519,8 @@ describe('Flyout', () => {
expect(resolveIndexPatternConflicts).toHaveBeenCalledWith(
component.instance().resolutions,
mockConflictedIndexPatterns,
true
true,
defaultProps.indexPatterns
);
expect(saveObjects).toHaveBeenCalledWith(
mockConflictedSavedObjectsLinkedToSavedSearches,

View file

@ -358,7 +358,8 @@ export class Flyout extends Component {
importCount += await resolveIndexPatternConflicts(
resolutions,
conflictedIndexPatterns,
isOverwriteAllChecked
isOverwriteAllChecked,
this.props.indexPatterns
);
}
this.setState({

View file

@ -84,7 +84,7 @@ describe('resolveSavedObjects', () => {
},
} as unknown) as IndexPatternsContract;
const services = [
const services = ([
{
type: 'search',
get: async () => {
@ -124,7 +124,7 @@ describe('resolveSavedObjects', () => {
};
},
},
] as SavedObjectLoader[];
] as unknown) as SavedObjectLoader[];
const overwriteAll = false;
@ -176,7 +176,7 @@ describe('resolveSavedObjects', () => {
},
} as unknown) as IndexPatternsContract;
const services = [
const services = ([
{
type: 'search',
get: async () => {
@ -217,7 +217,7 @@ describe('resolveSavedObjects', () => {
};
},
},
] as SavedObjectLoader[];
] as unknown) as SavedObjectLoader[];
const overwriteAll = false;
@ -237,33 +237,38 @@ describe('resolveSavedObjects', () => {
describe('resolveIndexPatternConflicts', () => {
it('should resave resolutions', async () => {
const hydrateIndexPattern = jest.fn();
const save = jest.fn();
const conflictedIndexPatterns = [
const conflictedIndexPatterns = ([
{
obj: {
searchSource: {
getOwnField: (field: string) => {
return field === 'index' ? '1' : undefined;
},
},
hydrateIndexPattern,
save,
},
doc: {
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
index: '1',
}),
},
},
},
},
{
obj: {
searchSource: {
getOwnField: (field: string) => {
return field === 'index' ? '3' : undefined;
},
},
hydrateIndexPattern,
save,
},
doc: {
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
index: '3',
}),
},
];
},
},
},
] as unknown) as Array<{ obj: SavedObject; doc: any }>;
const resolutions = [
{
@ -282,43 +287,49 @@ describe('resolveSavedObjects', () => {
const overwriteAll = false;
await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll);
expect(hydrateIndexPattern.mock.calls.length).toBe(2);
await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({
get: (id: string) => Promise.resolve({ id }),
} as unknown) as IndexPatternsContract);
expect(conflictedIndexPatterns[0].obj.searchSource!.getField('index')!.id).toEqual('2');
expect(conflictedIndexPatterns[1].obj.searchSource!.getField('index')!.id).toEqual('4');
expect(save.mock.calls.length).toBe(2);
expect(save).toHaveBeenCalledWith({ confirmOverwrite: !overwriteAll });
expect(hydrateIndexPattern).toHaveBeenCalledWith('2');
expect(hydrateIndexPattern).toHaveBeenCalledWith('4');
});
it('should resolve filter index conflicts', async () => {
const hydrateIndexPattern = jest.fn();
const save = jest.fn();
const conflictedIndexPatterns = [
const conflictedIndexPatterns = ([
{
obj: {
searchSource: {
getOwnField: (field: string) => {
return field === 'index' ? '1' : [{ meta: { index: 'filterIndex' } }];
},
setField: jest.fn(),
},
hydrateIndexPattern,
save,
},
doc: {
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
index: '1',
filter: [{ meta: { index: 'filterIndex' } }],
}),
},
},
},
},
{
obj: {
searchSource: {
getOwnField: (field: string) => {
return field === 'index' ? '3' : undefined;
},
},
hydrateIndexPattern,
save,
},
doc: {
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
index: '3',
}),
},
];
},
},
},
] as unknown) as Array<{ obj: SavedObject; doc: any }>;
const resolutions = [
{
@ -337,9 +348,11 @@ describe('resolveSavedObjects', () => {
const overwriteAll = false;
await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll);
await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({
get: (id: string) => Promise.resolve({ id }),
} as unknown) as IndexPatternsContract);
expect(conflictedIndexPatterns[0].obj.searchSource.setField).toHaveBeenCalledWith('filter', [
expect(conflictedIndexPatterns[0].obj.searchSource!.getField('filter')).toEqual([
{ meta: { index: 'newFilterIndex' } },
]);
expect(save.mock.calls.length).toBe(2);

View file

@ -18,12 +18,17 @@
*/
import { i18n } from '@kbn/i18n';
import { OverlayStart } from 'src/core/public';
import { cloneDeep } from 'lodash';
import { OverlayStart, SavedObjectReference } from 'src/core/public';
import {
SavedObject,
SavedObjectLoader,
} from '../../../../../../../../plugins/saved_objects/public';
import { IndexPatternsContract, IIndexPattern } from '../../../../../../../../plugins/data/public';
import {
IndexPatternsContract,
IIndexPattern,
createSearchSource,
} from '../../../../../../../../plugins/data/public';
type SavedObjectsRawDoc = Record<string, any>;
@ -126,7 +131,7 @@ async function importIndexPattern(
async function importDocument(obj: SavedObject, doc: SavedObjectsRawDoc, overwriteAll: boolean) {
await obj.applyESResp({
references: doc._references || [],
...doc,
...cloneDeep(doc),
});
return await obj.save({ confirmOverwrite: !overwriteAll });
}
@ -160,41 +165,57 @@ async function awaitEachItemInParallel<T, R>(list: T[], op: (item: T) => R) {
export async function resolveIndexPatternConflicts(
resolutions: Array<{ oldId: string; newId: string }>,
conflictedIndexPatterns: any[],
overwriteAll: boolean
overwriteAll: boolean,
indexPatterns: IndexPatternsContract
) {
let importCount = 0;
await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj }) => {
// Resolve search index reference:
let oldIndexId = obj.searchSource.getOwnField('index');
// Depending on the object, this can either be the raw id or the actual index pattern object
if (typeof oldIndexId !== 'string') {
oldIndexId = oldIndexId.id;
}
let resolution = resolutions.find(({ oldId }) => oldId === oldIndexId);
if (resolution) {
const newIndexId = resolution.newId;
await obj.hydrateIndexPattern(newIndexId);
await awaitEachItemInParallel(conflictedIndexPatterns, async ({ obj, doc }) => {
const serializedSearchSource = JSON.parse(
doc._source.kibanaSavedObjectMeta?.searchSourceJSON || '{}'
);
const oldIndexId = serializedSearchSource.index;
let allResolved = true;
const inlineResolution = resolutions.find(({ oldId }) => oldId === oldIndexId);
if (inlineResolution) {
serializedSearchSource.index = inlineResolution.newId;
} else {
allResolved = false;
}
// Resolve filter index reference:
const filter = (obj.searchSource.getOwnField('filter') || []).map((f: any) => {
const filter = (serializedSearchSource.filter || []).map((f: any) => {
if (!(f.meta && f.meta.index)) {
return f;
}
resolution = resolutions.find(({ oldId }) => oldId === f.meta.index);
const resolution = resolutions.find(({ oldId }) => oldId === f.meta.index);
return resolution ? { ...f, ...{ meta: { ...f.meta, index: resolution.newId } } } : f;
});
if (filter.length > 0) {
obj.searchSource.setField('filter', filter);
serializedSearchSource.filter = filter;
}
if (!resolution) {
const replacedReferences = (doc._references || []).map((reference: SavedObjectReference) => {
const resolution = resolutions.find(({ oldId }) => oldId === reference.id);
if (resolution) {
return { ...reference, id: resolution.newId };
} else {
allResolved = false;
}
return reference;
});
if (!allResolved) {
// The user decided to skip this conflict so do nothing
return;
}
obj.searchSource = await createSearchSource(indexPatterns)(
JSON.stringify(serializedSearchSource),
replacedReferences
);
if (await saveObject(obj, overwriteAll)) {
importCount++;
}

View file

@ -71,6 +71,7 @@ const getResolvedResults = deps => {
return createSavedSearchesLoader({
savedObjectsClient: core.savedObjects.client,
indexPatterns: data.indexPatterns,
search: data.search,
chrome: core.chrome,
overlays: core.overlays,
}).get(results.vis.data.savedSearchId);

View file

@ -28,6 +28,7 @@ const savedObjectsClient = npStart.core.savedObjects.client;
const services = {
savedObjectsClient,
indexPatterns: npStart.plugins.data.indexPatterns,
search: npStart.plugins.data.search,
chrome: npStart.core.chrome,
overlays: npStart.core.overlays,
};

View file

@ -72,9 +72,11 @@ export function setStartServices(npStart: NpStart) {
visualizationsServices.setAggs(npStart.plugins.data.search.aggs);
visualizationsServices.setOverlays(npStart.core.overlays);
visualizationsServices.setChrome(npStart.core.chrome);
visualizationsServices.setSearch(npStart.plugins.data.search);
const savedVisualizationsLoader = createSavedVisLoader({
savedObjectsClient: npStart.core.savedObjects.client,
indexPatterns: npStart.plugins.data.indexPatterns,
search: npStart.plugins.data.search,
chrome: npStart.core.chrome,
overlays: npStart.core.overlays,
visualizationTypes: visualizationsServices.getTypes(),

View file

@ -284,7 +284,7 @@ export class DashboardPlugin
const { notifications } = core;
const {
uiActions,
data: { indexPatterns },
data: { indexPatterns, search },
} = plugins;
const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings);
@ -300,6 +300,7 @@ export class DashboardPlugin
const savedDashboardLoader = createSavedDashboardLoader({
savedObjectsClient: core.savedObjects.client,
indexPatterns,
search,
chrome: core.chrome,
overlays: core.overlays,
});

View file

@ -18,13 +18,14 @@
*/
import { SavedObjectsClientContract, ChromeStart, OverlayStart } from 'kibana/public';
import { IndexPatternsContract } from '../../../../plugins/data/public';
import { DataPublicPluginStart, IndexPatternsContract } from '../../../../plugins/data/public';
import { SavedObjectLoader } from '../../../../plugins/saved_objects/public';
import { createSavedDashboardClass } from './saved_dashboard';
interface Services {
savedObjectsClient: SavedObjectsClientContract;
indexPatterns: IndexPatternsContract;
search: DataPublicPluginStart['search'];
chrome: ChromeStart;
overlays: OverlayStart;
}

View file

@ -366,6 +366,7 @@ export {
SearchStrategyProvider,
ISearchSource,
SearchSource,
createSearchSource,
SearchSourceFields,
EsQuerySortValue,
SortDirection,

View file

@ -155,7 +155,7 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
const query = this.queryService.start(savedObjects);
setQueryService(query);
const search = this.searchService.start(core);
const search = this.searchService.start(core, indexPatterns);
setSearchService(search);
uiActions.attachAction(APPLY_FILTER_TRIGGER, uiActions.getAction(ACTION_GLOBAL_APPLY_FILTER));

View file

@ -45,6 +45,7 @@ import * as React_2 from 'react';
import { Required } from '@kbn/utility-types';
import * as Rx from 'rxjs';
import { SavedObject as SavedObject_2 } from 'src/core/public';
import { SavedObjectReference } from 'kibana/public';
import { SavedObjectsClientContract } from 'src/core/public';
import { SearchParams } from 'elasticsearch';
import { SearchResponse as SearchResponse_2 } from 'elasticsearch';
@ -209,6 +210,9 @@ export const connectToQueryState: <S extends QueryState>({ timefilter: { timefil
// @public (undocumented)
export const createSavedQueryService: (savedObjectsClient: Pick<import("../../../../../core/public").SavedObjectsClient, "update" | "find" | "get" | "delete" | "create" | "bulkCreate" | "bulkGet" | "bulkUpdate">) => SavedQueryService;
// @public
export const createSearchSource: (indexPatterns: Pick<import("../../index_patterns/index_patterns").IndexPatternsService, "get" | "clearCache" | "getFieldsForTimePattern" | "getFieldsForWildcard" | "getIds" | "getTitles" | "getFields" | "getCache" | "getDefault" | "make">) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise<SearchSource>;
// Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@ -1677,6 +1681,10 @@ export class SearchSource {
// (undocumented)
history: SearchRequest[];
onRequestStart(handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise<unknown>): void;
serialize(): {
searchSourceJSON: string;
references: SavedObjectReference[];
};
// (undocumented)
setField<K extends keyof SearchSourceFields>(field: K, value: SearchSourceFields[K]): this;
// (undocumented)
@ -1891,21 +1899,21 @@ export type TSearchStrategyProvider<T extends TStrategyTypes> = (context: ISearc
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:383:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromEvent" needs to be exported by the entry point index.d.ts

View file

@ -54,6 +54,7 @@ export {
SearchSourceFields,
EsQuerySortValue,
SortDirection,
createSearchSource,
} from './search_source';
export { SearchInterceptor } from './search_interceptor';

View file

@ -33,6 +33,7 @@ export const searchStartMock: jest.Mocked<ISearchStart> = {
aggs: searchAggsStartMock(),
setInterceptor: jest.fn(),
search: jest.fn(),
createSearchSource: jest.fn(),
__LEGACY: {
AggConfig: jest.fn() as any,
AggType: jest.fn(),

View file

@ -25,6 +25,8 @@ import { TStrategyTypes } from './strategy_types';
import { getEsClient, LegacyApiCaller } from './es_client';
import { ES_SEARCH_STRATEGY, DEFAULT_SEARCH_STRATEGY } from '../../common/search';
import { esSearchStrategyProvider } from './es_search/es_search_strategy';
import { IndexPatternsContract } from '../index_patterns/index_patterns';
import { createSearchSource } from './search_source';
import { QuerySetup } from '../query/query_service';
import { GetInternalStartServicesFn } from '../types';
import { SearchInterceptor } from './search_interceptor';
@ -108,7 +110,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
};
}
public start(core: CoreStart): ISearchStart {
public start(core: CoreStart, indexPatterns: IndexPatternsContract): ISearchStart {
/**
* A global object that intercepts all searches and provides convenience methods for cancelling
* all pending search requests, as well as getting the number of pending search requests.
@ -145,6 +147,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
// TODO: should an intercepror have a destroy method?
this.searchInterceptor = searchInterceptor;
},
createSearchSource: createSearchSource(indexPatterns),
__LEGACY: {
esClient: this.esClient!,
AggConfig,

View file

@ -0,0 +1,151 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createSearchSource as createSearchSourceFactory } from './create_search_source';
import { IIndexPattern } from '../../../common/index_patterns';
import { IndexPatternsContract } from '../../index_patterns/index_patterns';
import { Filter } from '../../../common/es_query/filters';
describe('createSearchSource', function() {
let createSearchSource: ReturnType<typeof createSearchSourceFactory>;
const indexPatternMock: IIndexPattern = {} as IIndexPattern;
let indexPatternContractMock: jest.Mocked<IndexPatternsContract>;
beforeEach(() => {
indexPatternContractMock = ({
get: jest.fn().mockReturnValue(Promise.resolve(indexPatternMock)),
} as unknown) as jest.Mocked<IndexPatternsContract>;
createSearchSource = createSearchSourceFactory(indexPatternContractMock);
});
it('should fail if JSON is invalid', () => {
expect(createSearchSource('{', [])).rejects.toThrow();
expect(createSearchSource('0', [])).rejects.toThrow();
expect(createSearchSource('"abcdefg"', [])).rejects.toThrow();
});
it('should set fields', async () => {
const searchSource = await createSearchSource(
JSON.stringify({
highlightAll: true,
query: {
query: '',
language: 'kuery',
},
}),
[]
);
expect(searchSource.getOwnField('highlightAll')).toBe(true);
expect(searchSource.getOwnField('query')).toEqual({
query: '',
language: 'kuery',
});
});
it('should resolve referenced index pattern', async () => {
const searchSource = await createSearchSource(
JSON.stringify({
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
}),
[
{
id: '123-456',
type: 'index-pattern',
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
},
]
);
expect(indexPatternContractMock.get).toHaveBeenCalledWith('123-456');
expect(searchSource.getOwnField('index')).toBe(indexPatternMock);
});
it('should set filters and resolve referenced index patterns', async () => {
const searchSource = await createSearchSource(
JSON.stringify({
filter: [
{
meta: {
alias: null,
negate: false,
disabled: false,
type: 'phrase',
key: 'category.keyword',
params: {
query: "Men's Clothing",
},
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
},
query: {
match_phrase: {
'category.keyword': "Men's Clothing",
},
},
$state: {
store: 'appState',
},
},
],
}),
[
{
id: '123-456',
type: 'index-pattern',
name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index',
},
]
);
const filters = searchSource.getOwnField('filter') as Filter[];
expect(filters[0]).toMatchInlineSnapshot(`
Object {
"$state": Object {
"store": "appState",
},
"meta": Object {
"alias": null,
"disabled": false,
"index": "123-456",
"key": "category.keyword",
"negate": false,
"params": Object {
"query": "Men's Clothing",
},
"type": "phrase",
},
"query": Object {
"match_phrase": Object {
"category.keyword": "Men's Clothing",
},
},
}
`);
});
it('should migrate legacy queries on the fly', async () => {
const searchSource = await createSearchSource(
JSON.stringify({
highlightAll: true,
query: 'a:b',
}),
[]
);
expect(searchSource.getOwnField('query')).toEqual({
query: 'a:b',
language: 'lucene',
});
});
});

View file

@ -0,0 +1,113 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import { SavedObjectReference } from 'kibana/public';
import { migrateLegacyQuery } from '../../../../kibana_legacy/public';
import { InvalidJSONProperty } from '../../../../kibana_utils/public';
import { SearchSource } from './search_source';
import { IndexPatternsContract } from '../../index_patterns/index_patterns';
import { SearchSourceFields } from './types';
/**
* Deserializes a json string and a set of referenced objects to a `SearchSource` instance.
* Use this method to re-create the search source serialized using `searchSource.serialize`.
*
* This function is a factory function that returns the actual utility when calling it with the
* required service dependency (index patterns contract). A pre-wired version is also exposed in
* the start contract of the data plugin as part of the search service
*
* @param indexPatterns The index patterns contract of the data plugin
*
* @return Wired utility function taking two parameters `searchSourceJson`, the json string
* returned by `serializeSearchSource` and `references`, a list of references including the ones
* returned by `serializeSearchSource`.
*
* @public */
export const createSearchSource = (indexPatterns: IndexPatternsContract) => async (
searchSourceJson: string,
references: SavedObjectReference[]
) => {
const searchSource = new SearchSource();
// if we have a searchSource, set its values based on the searchSourceJson field
let searchSourceValues: Record<string, unknown>;
try {
searchSourceValues = JSON.parse(searchSourceJson);
} catch (e) {
throw new InvalidJSONProperty(
`Invalid JSON in search source. ${e.message} JSON: ${searchSourceJson}`
);
}
// This detects a scenario where documents with invalid JSON properties have been imported into the saved object index.
// (This happened in issue #20308)
if (!searchSourceValues || typeof searchSourceValues !== 'object') {
throw new InvalidJSONProperty('Invalid JSON in search source.');
}
// Inject index id if a reference is saved
if (searchSourceValues.indexRefName) {
const reference = references.find(ref => ref.name === searchSourceValues.indexRefName);
if (!reference) {
throw new Error(`Could not find reference for ${searchSourceValues.indexRefName}`);
}
searchSourceValues.index = reference.id;
delete searchSourceValues.indexRefName;
}
if (searchSourceValues.filter && Array.isArray(searchSourceValues.filter)) {
searchSourceValues.filter.forEach((filterRow: any) => {
if (!filterRow.meta || !filterRow.meta.indexRefName) {
return;
}
const reference = references.find((ref: any) => ref.name === filterRow.meta.indexRefName);
if (!reference) {
throw new Error(`Could not find reference for ${filterRow.meta.indexRefName}`);
}
filterRow.meta.index = reference.id;
delete filterRow.meta.indexRefName;
});
}
if (searchSourceValues.index && typeof searchSourceValues.index === 'string') {
searchSourceValues.index = await indexPatterns.get(searchSourceValues.index);
}
const searchSourceFields = searchSource.getFields();
const fnProps = _.transform(
searchSourceFields,
function(dynamic, val, name) {
if (_.isFunction(val) && name) dynamic[name] = val;
},
{}
);
// This assignment might hide problems because the type of values passed from the parsed JSON
// might not fit the SearchSourceFields interface.
const newFields: SearchSourceFields = _.defaults(searchSourceValues, fnProps);
searchSource.setFields(newFields);
const query = searchSource.getOwnField('query');
if (typeof query !== 'undefined') {
searchSource.setField('query', migrateLegacyQuery(query));
}
return searchSource;
};

View file

@ -18,4 +18,5 @@
*/
export * from './search_source';
export { createSearchSource } from './create_search_source';
export { SortDirection, EsQuerySortValue, SearchSourceFields } from './types';

View file

@ -37,4 +37,5 @@ export const searchSourceMock: MockedKeys<ISearchSource> = {
getSearchRequestBody: jest.fn(),
destroy: jest.fn(),
history: [],
serialize: jest.fn(),
};

View file

@ -18,7 +18,7 @@
*/
import { SearchSource } from './search_source';
import { IndexPattern } from '../..';
import { IndexPattern, SortDirection } from '../..';
import { mockDataServices } from '../aggs/test_helpers';
jest.mock('../fetch', () => ({
@ -150,4 +150,77 @@ describe('SearchSource', function() {
expect(parentFn).toBeCalledWith(searchSource, options);
});
});
describe('#serialize', function() {
it('should reference index patterns', () => {
const indexPattern123 = { id: '123' } as IndexPattern;
const searchSource = new SearchSource();
searchSource.setField('index', indexPattern123);
const { searchSourceJSON, references } = searchSource.serialize();
expect(references[0].id).toEqual('123');
expect(references[0].type).toEqual('index-pattern');
expect(JSON.parse(searchSourceJSON).indexRefName).toEqual(references[0].name);
});
it('should add other fields', () => {
const searchSource = new SearchSource();
searchSource.setField('highlightAll', true);
searchSource.setField('from', 123456);
const { searchSourceJSON } = searchSource.serialize();
expect(JSON.parse(searchSourceJSON).highlightAll).toEqual(true);
expect(JSON.parse(searchSourceJSON).from).toEqual(123456);
});
it('should omit sort and size', () => {
const searchSource = new SearchSource();
searchSource.setField('highlightAll', true);
searchSource.setField('from', 123456);
searchSource.setField('sort', { field: SortDirection.asc });
searchSource.setField('size', 200);
const { searchSourceJSON } = searchSource.serialize();
expect(Object.keys(JSON.parse(searchSourceJSON))).toEqual(['highlightAll', 'from']);
});
it('should serialize filters', () => {
const searchSource = new SearchSource();
const filter = [
{
query: 'query',
meta: {
alias: 'alias',
disabled: false,
negate: false,
},
},
];
searchSource.setField('filter', filter);
const { searchSourceJSON } = searchSource.serialize();
expect(JSON.parse(searchSourceJSON).filter).toEqual(filter);
});
it('should reference index patterns in filters separately from index field', () => {
const searchSource = new SearchSource();
const indexPattern123 = { id: '123' } as IndexPattern;
searchSource.setField('index', indexPattern123);
const filter = [
{
query: 'query',
meta: {
alias: 'alias',
disabled: false,
negate: false,
index: '456',
},
},
];
searchSource.setField('filter', filter);
const { searchSourceJSON, references } = searchSource.serialize();
expect(references[0].id).toEqual('123');
expect(references[0].type).toEqual('index-pattern');
expect(JSON.parse(searchSourceJSON).indexRefName).toEqual(references[0].name);
expect(references[1].id).toEqual('456');
expect(references[1].type).toEqual('index-pattern');
expect(JSON.parse(searchSourceJSON).filter[0].meta.indexRefName).toEqual(references[1].name);
});
});
});

View file

@ -70,6 +70,7 @@
*/
import _ from 'lodash';
import { SavedObjectReference } from 'kibana/public';
import { normalizeSortRequest } from './normalize_sort_request';
import { filterDocvalueFields } from './filter_docvalue_fields';
import { fieldWildcardFilter } from '../../../../kibana_utils/public';
@ -419,4 +420,85 @@ export class SearchSource {
return searchRequest;
}
/**
* Serializes the instance to a JSON string and a set of referenced objects.
* Use this method to get a representation of the search source which can be stored in a saved object.
*
* The references returned by this function can be mixed with other references in the same object,
* however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index`
* and `kibanaSavedObjectMeta.searchSourceJSON.filter[<number>].meta.index`.
*
* Using `createSearchSource`, the instance can be re-created.
* @param searchSource The search source to serialize
* @public */
public serialize() {
const references: SavedObjectReference[] = [];
const {
filter: originalFilters,
...searchSourceFields
}: Omit<SearchSourceFields, 'sort' | 'size'> = _.omit(this.getFields(), ['sort', 'size']);
let serializedSearchSourceFields: Omit<SearchSourceFields, 'sort' | 'size' | 'filter'> & {
indexRefName?: string;
filter?: Array<Omit<Filter, 'meta'> & { meta: Filter['meta'] & { indexRefName?: string } }>;
} = searchSourceFields;
if (searchSourceFields.index) {
const indexId = searchSourceFields.index.id!;
const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index';
references.push({
name: refName,
type: 'index-pattern',
id: indexId,
});
serializedSearchSourceFields = {
...serializedSearchSourceFields,
indexRefName: refName,
index: undefined,
};
}
if (originalFilters) {
const filters = this.getFilters(originalFilters);
serializedSearchSourceFields = {
...serializedSearchSourceFields,
filter: filters.map((filterRow, i) => {
if (!filterRow.meta || !filterRow.meta.index) {
return filterRow;
}
const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`;
references.push({
name: refName,
type: 'index-pattern',
id: filterRow.meta.index,
});
return {
...filterRow,
meta: {
...filterRow.meta,
indexRefName: refName,
index: undefined,
},
};
}),
};
}
return { searchSourceJSON: JSON.stringify(serializedSearchSourceFields), references };
}
private getFilters(filterField: SearchSourceFields['filter']): Filter[] {
if (!filterField) {
return [];
}
if (Array.isArray(filterField)) {
return filterField;
}
if (_.isFunction(filterField)) {
return this.getFilters(filterField());
}
return [filterField];
}
}

View file

@ -18,6 +18,7 @@
*/
import { CoreStart } from 'kibana/public';
import { createSearchSource } from './search_source';
import { SearchAggsSetup, SearchAggsStart, SearchAggsStartLegacy } from './aggs';
import { ISearch, ISearchGeneric } from './i_search';
import { TStrategyTypes } from './strategy_types';
@ -89,5 +90,6 @@ export interface ISearchStart {
aggs: SearchAggsStart;
setInterceptor: (searchInterceptor: SearchInterceptor) => void;
search: ISearchGeneric;
createSearchSource: ReturnType<typeof createSearchSource>;
__LEGACY: ISearchStartLegacy & SearchAggsStartLegacy;
}

View file

@ -39,6 +39,7 @@ export class SavedObjectsPublicPlugin
SavedObjectClass: createSavedObjectClass({
indexPatterns: data.indexPatterns,
savedObjectsClient: core.savedObjects.client,
search: data.search,
chrome: core.chrome,
overlays: core.overlays,
}),

View file

@ -18,9 +18,8 @@
*/
import _ from 'lodash';
import { EsResponse, SavedObject, SavedObjectConfig } from '../../types';
import { parseSearchSource } from './parse_search_source';
import { expandShorthand, SavedObjectNotFound } from '../../../../kibana_utils/public';
import { IndexPattern } from '../../../../data/public';
import { DataPublicPluginStart, IndexPattern } from '../../../../data/public';
/**
* A given response of and ElasticSearch containing a plain saved object is applied to the given
@ -29,13 +28,13 @@ import { IndexPattern } from '../../../../data/public';
export async function applyESResp(
resp: EsResponse,
savedObject: SavedObject,
config: SavedObjectConfig
config: SavedObjectConfig,
createSearchSource: DataPublicPluginStart['search']['createSearchSource']
) {
const mapping = expandShorthand(config.mapping);
const esType = config.type || '';
savedObject._source = _.cloneDeep(resp._source);
const injectReferences = config.injectReferences;
const hydrateIndexPattern = savedObject.hydrateIndexPattern!;
if (typeof resp.found === 'boolean' && !resp.found) {
throw new SavedObjectNotFound(esType, savedObject.id || '');
}
@ -64,13 +63,34 @@ export async function applyESResp(
_.assign(savedObject, savedObject._source);
savedObject.lastSavedTitle = savedObject.title;
await parseSearchSource(savedObject, esType, meta.searchSourceJSON, resp.references);
await hydrateIndexPattern();
if (config.searchSource) {
try {
savedObject.searchSource = await createSearchSource(meta.searchSourceJSON, resp.references);
} catch (error) {
if (
error.constructor.name === 'SavedObjectNotFound' &&
error.savedObjectType === 'index-pattern'
) {
// if parsing the search source fails because the index pattern wasn't found,
// remember the reference - this is required for error handling on legacy imports
savedObject.unresolvedIndexPatternReference = {
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
id: JSON.parse(meta.searchSourceJSON).index,
type: 'index-pattern',
};
}
throw error;
}
}
if (injectReferences && resp.references && resp.references.length > 0) {
injectReferences(savedObject, resp.references);
}
if (typeof config.afterESResp === 'function') {
savedObject = await config.afterESResp(savedObject);
}
return savedObject;
}

View file

@ -81,7 +81,8 @@ export function buildSavedObject(
*/
savedObject.init = _.once(() => intializeSavedObject(savedObject, savedObjectsClient, config));
savedObject.applyESResp = (resp: EsResponse) => applyESResp(resp, savedObject, config);
savedObject.applyESResp = (resp: EsResponse) =>
applyESResp(resp, savedObject, config, services.search.createSearchSource);
/**
* Serialize this object

View file

@ -31,25 +31,19 @@ export async function hydrateIndexPattern(
indexPatterns: IndexPatternsContract,
config: SavedObjectConfig
) {
const clearSavedIndexPattern = !!config.clearSavedIndexPattern;
const indexPattern = config.indexPattern;
if (!savedObject.searchSource) {
return null;
}
if (clearSavedIndexPattern) {
savedObject.searchSource!.setField('index', undefined);
return null;
}
const index = id || indexPattern || savedObject.searchSource!.getOwnField('index');
const index = id || indexPattern || savedObject.searchSource.getOwnField('index');
if (typeof index !== 'string' || !index) {
return null;
}
const indexObj = await indexPatterns.get(index);
savedObject.searchSource!.setField('index', indexObj);
savedObject.searchSource.setField('index', indexObj);
return indexObj;
}

View file

@ -1,97 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import { migrateLegacyQuery } from '../../../../kibana_legacy/public';
import { SavedObject } from '../../types';
import { InvalidJSONProperty } from '../../../../kibana_utils/public';
export function parseSearchSource(
savedObject: SavedObject,
esType: string,
searchSourceJson: string,
references: any[]
) {
if (!savedObject.searchSource) return;
// if we have a searchSource, set its values based on the searchSourceJson field
let searchSourceValues: Record<string, any>;
try {
searchSourceValues = JSON.parse(searchSourceJson);
} catch (e) {
throw new InvalidJSONProperty(
`Invalid JSON in ${esType} "${savedObject.id}". ${e.message} JSON: ${searchSourceJson}`
);
}
// This detects a scenario where documents with invalid JSON properties have been imported into the saved object index.
// (This happened in issue #20308)
if (!searchSourceValues || typeof searchSourceValues !== 'object') {
throw new InvalidJSONProperty(`Invalid searchSourceJSON in ${esType} "${savedObject.id}".`);
}
// Inject index id if a reference is saved
if (searchSourceValues.indexRefName) {
const reference = references.find(
(ref: Record<string, any>) => ref.name === searchSourceValues.indexRefName
);
if (!reference) {
throw new Error(
`Could not find reference for ${
searchSourceValues.indexRefName
} on ${savedObject.getEsType()} ${savedObject.id}`
);
}
searchSourceValues.index = reference.id;
delete searchSourceValues.indexRefName;
}
if (searchSourceValues.filter) {
searchSourceValues.filter.forEach((filterRow: any) => {
if (!filterRow.meta || !filterRow.meta.indexRefName) {
return;
}
const reference = references.find((ref: any) => ref.name === filterRow.meta.indexRefName);
if (!reference) {
throw new Error(
`Could not find reference for ${
filterRow.meta.indexRefName
} on ${savedObject.getEsType()}`
);
}
filterRow.meta.index = reference.id;
delete filterRow.meta.indexRefName;
});
}
const searchSourceFields = savedObject.searchSource.getFields();
const fnProps = _.transform(
searchSourceFields,
function(dynamic: Record<string, any>, val: any, name: string | undefined) {
if (_.isFunction(val) && name) dynamic[name] = val;
},
{}
);
savedObject.searchSource.setFields(_.defaults(searchSourceValues, fnProps));
const query = savedObject.searchSource.getOwnField('query');
if (typeof query !== 'undefined') {
savedObject.searchSource.setField('query', migrateLegacyQuery(query));
}
}

View file

@ -17,7 +17,6 @@
* under the License.
*/
import _ from 'lodash';
import angular from 'angular';
import { SavedObject, SavedObjectConfig } from '../../types';
import { expandShorthand } from '../../../../kibana_utils/public';
@ -41,57 +40,16 @@ export function serializeSavedObject(savedObject: SavedObject, config: SavedObje
});
if (savedObject.searchSource) {
let searchSourceFields: Record<string, any> = _.omit(savedObject.searchSource.getFields(), [
'sort',
'size',
]);
if (searchSourceFields.index) {
// searchSourceFields.index will normally be an IndexPattern, but can be a string in two scenarios:
// (1) `init()` (and by extension `hydrateIndexPattern()`) hasn't been called on Saved Object
// (2) The IndexPattern doesn't exist, so we fail to resolve it in `hydrateIndexPattern()`
const indexId =
typeof searchSourceFields.index === 'string'
? searchSourceFields.index
: searchSourceFields.index.id;
const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index';
references.push({
name: refName,
type: 'index-pattern',
id: indexId,
});
searchSourceFields = {
...searchSourceFields,
indexRefName: refName,
index: undefined,
};
const {
searchSourceJSON,
references: searchSourceReferences,
} = savedObject.searchSource.serialize();
attributes.kibanaSavedObjectMeta = { searchSourceJSON };
references.push(...searchSourceReferences);
}
if (searchSourceFields.filter) {
searchSourceFields = {
...searchSourceFields,
filter: searchSourceFields.filter.map((filterRow: any, i: number) => {
if (!filterRow.meta || !filterRow.meta.index) {
return filterRow;
}
const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`;
references.push({
name: refName,
type: 'index-pattern',
id: filterRow.meta.index,
});
return {
...filterRow,
meta: {
...filterRow.meta,
indexRefName: refName,
index: undefined,
},
};
}),
};
}
attributes.kibanaSavedObjectMeta = {
searchSourceJSON: angular.toJson(searchSourceFields),
};
if (savedObject.unresolvedIndexPatternReference) {
references.push(savedObject.unresolvedIndexPatternReference);
}
return { attributes, references };

View file

@ -103,9 +103,11 @@ describe('Saved Object', () => {
}
beforeEach(() => {
(dataStartMock.search.createSearchSource as jest.Mock).mockReset();
SavedObjectClass = createSavedObjectClass({
savedObjectsClient: savedObjectsClientStub,
indexPatterns: dataStartMock.indexPatterns,
search: dataStartMock.search,
} as SavedObjectKibanaServices);
});
@ -269,7 +271,7 @@ describe('Saved Object', () => {
);
});
it('when index exists in searchSourceJSON', () => {
it('when search source references saved object', () => {
const id = '123';
stubESResponse(getMockedDocResponse(id));
return createInitializedSavedObject({ type: 'dashboard', searchSource: true }).then(
@ -409,18 +411,17 @@ describe('Saved Object', () => {
});
});
it('throws error invalid JSON is detected', async () => {
it('forwards thrown exceptions from createSearchSource', async () => {
(dataStartMock.search.createSearchSource as jest.Mock).mockImplementation(() => {
throw new InvalidJSONProperty('');
});
const savedObject = await createInitializedSavedObject({
type: 'dashboard',
searchSource: true,
});
const response = {
found: true,
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: '"{\\n \\"filter\\": []\\n}"',
},
},
_source: {},
};
try {
@ -586,14 +587,10 @@ describe('Saved Object', () => {
});
});
it('injects references from searchSourceJSON', async () => {
it('passes references to search source parsing function', async () => {
const savedObject = new SavedObjectClass({ type: 'dashboard', searchSource: true });
return savedObject.init!().then(() => {
const response = {
found: true,
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
const searchSourceJSON = JSON.stringify({
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
filter: [
{
@ -602,7 +599,12 @@ describe('Saved Object', () => {
},
},
],
}),
});
const response = {
found: true,
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON,
},
},
references: [
@ -619,16 +621,10 @@ describe('Saved Object', () => {
],
};
savedObject.applyESResp(response);
expect(savedObject.searchSource!.getFields()).toEqual({
index: 'my-index-1',
filter: [
{
meta: {
index: 'my-index-2',
},
},
],
});
expect(dataStartMock.search.createSearchSource).toBeCalledWith(
searchSourceJSON,
response.references
);
});
});
});

View file

@ -24,7 +24,12 @@ import {
SavedObjectAttributes,
SavedObjectReference,
} from 'kibana/public';
import { IIndexPattern, IndexPatternsContract, ISearchSource } from '../../data/public';
import {
DataPublicPluginStart,
IIndexPattern,
IndexPatternsContract,
ISearchSource,
} from '../../data/public';
export interface SavedObject {
_serialize: () => { attributes: SavedObjectAttributes; references: SavedObjectReference[] };
@ -49,6 +54,7 @@ export interface SavedObject {
searchSource?: ISearchSource;
showInRecentlyAccessed: boolean;
title: string;
unresolvedIndexPatternReference?: SavedObjectReference;
}
export interface SavedObjectSaveOpts {
@ -65,6 +71,7 @@ export interface SavedObjectCreationOpts {
export interface SavedObjectKibanaServices {
savedObjectsClient: SavedObjectsClientContract;
indexPatterns: IndexPatternsContract;
search: DataPublicPluginStart['search'];
chrome: ChromeStart;
overlays: OverlayStart;
}
@ -72,7 +79,6 @@ export interface SavedObjectKibanaServices {
export interface SavedObjectConfig {
// is only used by visualize
afterESResp?: (savedObject: SavedObject) => Promise<SavedObject>;
clearSavedIndexPattern?: boolean;
defaults?: any;
extractReferences?: (opts: {
attributes: SavedObjectAttributes;

View file

@ -26,6 +26,7 @@ import {
setCapabilities,
setHttp,
setIndexPatterns,
setSearch,
setSavedObjects,
setUsageCollector,
setFilterManager,
@ -140,6 +141,7 @@ export class VisualizationsPlugin
setHttp(core.http);
setSavedObjects(core.savedObjects);
setIndexPatterns(data.indexPatterns);
setSearch(data.search);
setFilterManager(data.query.filterManager);
setExpressions(expressions);
setUiActions(uiActions);
@ -150,6 +152,7 @@ export class VisualizationsPlugin
const savedVisualizationsLoader = createSavedVisLoader({
savedObjectsClient: core.savedObjects.client,
indexPatterns: data.indexPatterns,
search: data.search,
chrome: core.chrome,
overlays: core.overlays,
visualizationTypes: types,

View file

@ -35,7 +35,7 @@ import { extractReferences, injectReferences } from './saved_visualization_refer
import { IIndexPattern, ISearchSource, SearchSource } from '../../../../plugins/data/public';
import { ISavedVis, SerializedVis } from '../types';
import { createSavedSearchesLoader } from '../../../../plugins/discover/public';
import { getChrome, getOverlays, getIndexPatterns, getSavedObjects } from '../services';
import { getChrome, getOverlays, getIndexPatterns, getSavedObjects, getSearch } from '../services';
export const convertToSerializedVis = async (savedVis: ISavedVis): Promise<SerializedVis> => {
const { visState } = savedVis;
@ -87,6 +87,7 @@ const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?:
const savedSearch = await createSavedSearchesLoader({
savedObjectsClient: getSavedObjects().client,
indexPatterns: getIndexPatterns(),
search: getSearch(),
chrome: getChrome(),
overlays: getOverlays(),
}).get(savedSearchId);

View file

@ -63,6 +63,8 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter<IndexPatt
'IndexPatterns'
);
export const [getSearch, setSearch] = createGetterSetter<DataPublicPluginStart['search']>('Search');
export const [getUsageCollector, setUsageCollector] = createGetterSetter<UsageCollectionSetup>(
'UsageCollection'
);

View file

@ -17,6 +17,7 @@ module.service('gisMapSavedObjectLoader', function() {
const services = {
savedObjectsClient,
indexPatterns: npStart.plugins.data.indexPatterns,
search: npStart.plugins.data.search,
chrome: npStart.core.chrome,
overlays: npStart.core.overlays,
};

View file

@ -29,6 +29,7 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => {
const savedSearches = createSavedSearchesLoader({
savedObjectsClient,
indexPatterns,
search: appDeps.data.search,
chrome: appDeps.chrome,
overlays: appDeps.overlays,
});