[saved search] Remove saved object client from data views plugin for saved search usage (#159109)

## Summary

Previously the data plugin relied on the data view plugin to load saved
searches since the saved searches depend upon the data plugin and
circular dependencies needed to be avoided. This is innovative and
perhaps a bit crazy.

What this PR does
- Data view api no longer loads saved searches, removing browser saved
object client usage
- Moves `kibana_context` expression and getKibanaContext function from
data plugin to saved search plugin since it loads saved searches
- Rename data views `SavedObjectsClientCommon` to `PersistenceAPI` -
this is the abstraction around saved object loading that no longer is
exclusive to the saved objects api.
- Adds saved search server api (plugin contract) for loading saved
searches.
- Functional tests on browser and server for kibana_context expression
when loading saved searches

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Matthew Kime 2023-07-11 08:23:46 -05:00 committed by GitHub
parent 672c90a9c1
commit d9d1404119
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 504 additions and 374 deletions

View file

@ -6,22 +6,15 @@
* Side Public License, v 1.
*/
import { isEqual, uniqBy } from 'lodash';
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition, ExecutionContext } from '@kbn/expressions-plugin/common';
import { Adapters } from '@kbn/inspector-plugin/common';
import { Filter, fromCombinedFilter } from '@kbn/es-query';
import { Query, uniqFilters } from '@kbn/es-query';
import { unboxExpressionValue } from '@kbn/expressions-plugin/common';
import { SavedObjectReference } from '@kbn/core/types';
import { SavedObjectsClientCommon } from '@kbn/data-views-plugin/common';
import { ExecutionContextSearch, KibanaContext, KibanaFilter } from './kibana_context_type';
import { KibanaQueryOutput } from './kibana_context_type';
import { KibanaTimerangeOutput } from './timerange';
export interface KibanaContextStartDependencies {
savedObjectsClient: SavedObjectsClientCommon;
}
import {
KibanaTimerangeOutput,
ExecutionContextSearch,
KibanaContext,
KibanaFilter,
KibanaQueryOutput,
} from '../..';
interface Arguments {
q?: KibanaQueryOutput[] | null;
@ -37,125 +30,3 @@ export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<
Promise<KibanaContext>,
ExecutionContext<Adapters, ExecutionContextSearch>
>;
const getParsedValue = (data: any, defaultValue: any) =>
typeof data === 'string' && data.length ? JSON.parse(data) || defaultValue : defaultValue;
const mergeQueries = (first: Query | Query[] = [], second: Query | Query[]) =>
uniqBy<Query>(
[...(Array.isArray(first) ? first : [first]), ...(Array.isArray(second) ? second : [second])],
(n: any) => JSON.stringify(n.query)
);
export const getKibanaContextFn = (
getStartDependencies: (
getKibanaRequest: ExecutionContext['getKibanaRequest']
) => Promise<KibanaContextStartDependencies>
) => {
const kibanaContextFunction: ExpressionFunctionKibanaContext = {
name: 'kibana_context',
type: 'kibana_context',
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('data.search.functions.kibana_context.help', {
defaultMessage: 'Updates kibana global context',
}),
args: {
q: {
types: ['kibana_query', 'null'],
multi: true,
aliases: ['query', '_'],
help: i18n.translate('data.search.functions.kibana_context.q.help', {
defaultMessage: 'Specify Kibana free form text query',
}),
},
filters: {
types: ['kibana_filter', 'null'],
multi: true,
help: i18n.translate('data.search.functions.kibana_context.filters.help', {
defaultMessage: 'Specify Kibana generic filters',
}),
},
timeRange: {
types: ['timerange', 'null'],
default: null,
help: i18n.translate('data.search.functions.kibana_context.timeRange.help', {
defaultMessage: 'Specify Kibana time range filter',
}),
},
savedSearchId: {
types: ['string', 'null'],
default: null,
help: i18n.translate('data.search.functions.kibana_context.savedSearchId.help', {
defaultMessage: 'Specify saved search ID to be used for queries and filters',
}),
},
},
extract(state) {
const references: SavedObjectReference[] = [];
if (state.savedSearchId.length && typeof state.savedSearchId[0] === 'string') {
const refName = 'kibana_context.savedSearchId';
references.push({
name: refName,
type: 'search',
id: state.savedSearchId[0] as string,
});
return {
state: {
...state,
savedSearchId: [refName],
},
references,
};
}
return { state, references };
},
inject(state, references) {
const reference = references.find((r) => r.name === 'kibana_context.savedSearchId');
if (reference) {
state.savedSearchId[0] = reference.id;
}
return state;
},
async fn(input, args, { getKibanaRequest }) {
const { savedObjectsClient } = await getStartDependencies(getKibanaRequest);
const timeRange = args.timeRange || input?.timeRange;
let queries = mergeQueries(input?.query, args?.q?.filter(Boolean) || []);
const filterFromArgs = (args?.filters?.map(unboxExpressionValue) || []) as Filter[];
let filters = [...(input?.filters || [])];
if (args.savedSearchId) {
const obj = await savedObjectsClient.getSavedSearch(args.savedSearchId);
const search = (obj.attributes as any).kibanaSavedObjectMeta.searchSourceJSON as string;
const { query, filter } = getParsedValue(search, {});
if (query) {
queries = mergeQueries(queries, query);
}
if (filter) {
filters = [...filters, ...(Array.isArray(filter) ? filter : [filter])];
}
}
const uniqueArgFilters = filterFromArgs.filter(
(argF) =>
!filters.some((f) => {
return isEqual(fromCombinedFilter(f).query, argF.query);
})
);
filters = [...filters, ...uniqueArgFilters];
return {
type: 'kibana_context',
query: queries,
filters: uniqFilters(filters.filter((f: any) => !f.meta?.disabled)),
timeRange,
};
},
};
return kibanaContextFunction;
};

View file

@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
import { Filter } from '@kbn/es-query';
import { ExpressionValueBoxed, ExpressionValueFilter } from '@kbn/expressions-plugin/common';
import { ExpressionValueBoxed } from '@kbn/expressions-plugin/common';
import { Query, TimeRange } from '../../query';
import { adaptToExpressionValueFilter, DataViewField } from '../..';
import { DataViewField } from '../..';
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ExecutionContextSearch = {
@ -30,29 +30,3 @@ export type KibanaField = ExpressionValueBoxed<'kibana_field', DataViewField>;
// TODO: These two are exported for legacy reasons - remove them eventually.
export type KIBANA_CONTEXT_NAME = 'kibana_context';
export type KibanaContext = ExpressionValueSearchContext;
export const kibanaContext = {
name: 'kibana_context',
from: {
null: () => {
return {
type: 'kibana_context',
};
},
},
to: {
null: () => {
return {
type: 'null',
};
},
filter: (input: KibanaContext): ExpressionValueFilter => {
const { filters = [] } = input;
return {
type: 'filter',
filterType: 'filter',
and: filters.map(adaptToExpressionValueFilter),
};
},
},
};

View file

@ -38,7 +38,6 @@ import {
ipRangeFunction,
ISearchGeneric,
kibana,
kibanaContext,
kibanaFilterFunction,
kibanaTimerangeFunction,
kqlFunction,
@ -65,7 +64,7 @@ import { DataPublicPluginStart, DataStartDependencies } from '../types';
import { AggsService } from './aggs';
import { createUsageCollector, SearchUsageCollector } from './collectors';
import { getEql, getEsaggs, getEsdsl, getEssql } from './expressions';
import { getKibanaContext } from './expressions/kibana_context';
import { handleWarnings } from './fetch/handle_warnings';
import { ISearchInterceptor, SearchInterceptor } from './search_interceptor';
import { ISessionsClient, ISessionService, SessionsClient, SessionService } from './session';
@ -143,11 +142,6 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
})
);
expressions.registerFunction(kibana);
expressions.registerFunction(
getKibanaContext({ getStartServices } as {
getStartServices: StartServicesAccessor<DataStartDependencies, DataPublicPluginStart>;
})
);
expressions.registerFunction(cidrFunction);
expressions.registerFunction(dateRangeFunction);
expressions.registerFunction(extendedBoundsFunction);
@ -167,7 +161,6 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
expressions.registerFunction(removeFilterFunction);
expressions.registerFunction(selectFilterFunction);
expressions.registerFunction(phraseFilterFunction);
expressions.registerType(kibanaContext);
expressions.registerFunction(
getEsdsl({ getStartServices } as {

View file

@ -67,7 +67,6 @@ import {
ipRangeFunction,
ISearchOptions,
kibana,
kibanaContext,
kibanaFilterFunction,
kibanaTimerangeFunction,
kqlFunction,
@ -94,7 +93,6 @@ import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn';
import { ConfigSchema } from '../../config';
import { SearchSessionService } from './session';
import { registerBsearchRoute } from './routes/bsearch';
import { getKibanaContext } from './expressions/kibana_context';
import { enhancedEsSearchStrategyProvider } from './strategies/ese_search';
import { eqlSearchStrategyProvider } from './strategies/eql_search';
import { NoSearchIdInSessionError } from './errors/no_search_id_in_session';
@ -233,7 +231,6 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
expressions.registerFunction(luceneFunction);
expressions.registerFunction(kqlFunction);
expressions.registerFunction(kibanaTimerangeFunction);
expressions.registerFunction(getKibanaContext({ getStartServices: core.getStartServices }));
expressions.registerFunction(fieldFunction);
expressions.registerFunction(numericalRangeFunction);
expressions.registerFunction(rangeFunction);
@ -244,7 +241,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
expressions.registerFunction(removeFilterFunction);
expressions.registerFunction(selectFilterFunction);
expressions.registerFunction(phraseFilterFunction);
expressions.registerType(kibanaContext);
expressions.registerType(esRawResponse);
expressions.registerType(eqlRawResponse);

View file

@ -49,7 +49,7 @@
"@kbn/core-application-browser",
"@kbn/core-saved-objects-server",
"@kbn/core-saved-objects-utils-server",
"@kbn/data-service",
"@kbn/data-service"
],
"exclude": [
"target/**/*",

View file

@ -13,7 +13,7 @@ import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
import {
UiSettingsCommon,
SavedObjectsClientCommon,
PersistenceAPI,
SavedObject,
DataViewSpec,
IDataViewsApiClient,
@ -60,7 +60,7 @@ const savedObject = {
describe('IndexPatterns', () => {
let indexPatterns: DataViewsService;
let indexPatternsNoAccess: DataViewsService;
let savedObjectsClient: SavedObjectsClientCommon;
let savedObjectsClient: PersistenceAPI;
let SOClientGetDelay = 0;
let apiClient: IDataViewsApiClient;
const uiSettings = {
@ -73,7 +73,7 @@ describe('IndexPatterns', () => {
beforeEach(() => {
jest.clearAllMocks();
savedObjectsClient = {} as SavedObjectsClientCommon;
savedObjectsClient = {} as PersistenceAPI;
savedObjectsClient.find = jest.fn(
() => Promise.resolve([indexPatternObj]) as Promise<Array<SavedObject<any>>>
);
@ -107,7 +107,7 @@ describe('IndexPatterns', () => {
indexPatterns = new DataViewsService({
uiSettings,
savedObjectsClient: savedObjectsClient as unknown as SavedObjectsClientCommon,
savedObjectsClient: savedObjectsClient as unknown as PersistenceAPI,
apiClient,
fieldFormats,
onNotification: () => {},
@ -119,7 +119,7 @@ describe('IndexPatterns', () => {
indexPatternsNoAccess = new DataViewsService({
uiSettings,
savedObjectsClient: savedObjectsClient as unknown as SavedObjectsClientCommon,
savedObjectsClient: savedObjectsClient as unknown as PersistenceAPI,
apiClient,
fieldFormats,
onNotification: () => {},

View file

@ -11,7 +11,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types';
import { castEsToKbnFieldTypeName } from '@kbn/field-types';
import { FieldFormatsStartCommon, FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common';
import { v4 as uuidv4 } from 'uuid';
import { SavedObjectsClientCommon } from '../types';
import { PersistenceAPI } from '../types';
import { createDataViewCache } from '.';
import type { RuntimeField, RuntimeFieldSpec, RuntimeType } from '../types';
@ -89,7 +89,7 @@ export interface DataViewsServiceDeps {
/**
* Saved objects client interface wrapped in a common interface
*/
savedObjectsClient: SavedObjectsClientCommon;
savedObjectsClient: PersistenceAPI;
/**
* Wrapper around http call functionality so it can be used on client or server
*/
@ -292,7 +292,7 @@ export interface DataViewsServicePublicMethods {
*/
export class DataViewsService {
private config: UiSettingsCommon;
private savedObjectsClient: SavedObjectsClientCommon;
private savedObjectsClient: PersistenceAPI;
private savedObjectsCache?: Array<SavedObject<DataViewSavedObjectAttrs>> | null;
private apiClient: IDataViewsApiClient;
private fieldFormats: FieldFormatsStartCommon;

View file

@ -38,7 +38,7 @@ export type {
OnNotification,
OnError,
UiSettingsCommon,
SavedObjectsClientCommon,
PersistenceAPI,
GetFieldsOptions,
IDataViewsApiClient,
SavedObject,

View file

@ -252,10 +252,10 @@ export interface SavedObjectsClientCommonFindArgs {
}
/**
* Common interface for the saved objects client
* Common interface for the saved objects client on server and content management in browser
* @public
*/
export interface SavedObjectsClientCommon {
export interface PersistenceAPI {
/**
* Search for saved objects
* @param options - options for search
@ -269,14 +269,6 @@ export interface SavedObjectsClientCommon {
* @param id - id of saved object
*/
get: (id: string) => Promise<SavedObject<DataViewAttributes>>;
/**
* Update a saved object by id
* @param type - type of saved object
* @param id - id of saved object
* @param attributes - attributes to update
* @param options - client options
*/
getSavedSearch: (id: string) => Promise<SavedObject>;
/**
* Update a saved object by id
* @param type - type of saved object

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import type { SavedObjectsClientCommon } from './types';
import type { PersistenceAPI } from './types';
/**
* Returns an object matching a given name
@ -15,7 +15,7 @@ import type { SavedObjectsClientCommon } from './types';
* @param name {string}
* @returns {SavedObject|undefined}
*/
export async function findByName(client: SavedObjectsClientCommon, name: string) {
export async function findByName(client: PersistenceAPI, name: string) {
if (name) {
const savedObjects = await client.find({
perPage: 10,

View file

@ -6,13 +6,11 @@
* Side Public License, v 1.
*/
import { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper';
import { ContentMagementWrapper } from './content_management_wrapper';
import { ContentClient } from '@kbn/content-management-plugin/public';
import { savedObjectsServiceMock } from '@kbn/core/public/mocks';
import { DataViewSavedObjectConflictError } from '../common';
describe('SavedObjectsClientPublicToCommon', () => {
const soClient = savedObjectsServiceMock.createStartContract().client;
describe('ContentMagementWrapper', () => {
const cmClient = {} as ContentClient;
test('get saved object - exactMatch', async () => {
@ -22,7 +20,7 @@ describe('SavedObjectsClientPublicToCommon', () => {
cmClient.get = jest
.fn()
.mockResolvedValue({ meta: { outcome: 'exactMatch' }, item: mockedSavedObject });
const service = new SavedObjectsClientPublicToCommon(cmClient, soClient);
const service = new ContentMagementWrapper(cmClient);
const result = await service.get('1');
expect(result).toStrictEqual(mockedSavedObject);
});
@ -34,7 +32,7 @@ describe('SavedObjectsClientPublicToCommon', () => {
cmClient.get = jest
.fn()
.mockResolvedValue({ meta: { outcome: 'aliasMatch' }, item: mockedSavedObject });
const service = new SavedObjectsClientPublicToCommon(cmClient, soClient);
const service = new ContentMagementWrapper(cmClient);
const result = await service.get('1');
expect(result).toStrictEqual(mockedSavedObject);
});
@ -47,7 +45,7 @@ describe('SavedObjectsClientPublicToCommon', () => {
cmClient.get = jest
.fn()
.mockResolvedValue({ meta: { outcome: 'conflict' }, item: mockedSavedObject });
const service = new SavedObjectsClientPublicToCommon(cmClient, soClient);
const service = new ContentMagementWrapper(cmClient);
await expect(service.get('1')).rejects.toThrow(DataViewSavedObjectConflictError);
});

View file

@ -8,12 +8,11 @@
import type { ContentClient } from '@kbn/content-management-plugin/public';
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
import type { SavedObjectsClientContract } from '@kbn/core/public';
import { DataViewSavedObjectConflictError } from '../common/errors';
import {
DataViewAttributes,
SavedObject,
SavedObjectsClientCommon,
PersistenceAPI,
SavedObjectsClientCommonFindArgs,
} from '../common/types';
@ -21,15 +20,11 @@ import type { DataViewCrudTypes } from '../common/content_management';
import { DataViewSOType } from '../common/content_management';
type SOClient = Pick<SavedObjectsClientContract, 'resolve'>;
export class SavedObjectsClientPublicToCommon implements SavedObjectsClientCommon {
export class ContentMagementWrapper implements PersistenceAPI {
private contentManagementClient: ContentClient;
private savedObjectClient: SOClient;
constructor(contentManagementClient: ContentClient, savedObjectClient: SOClient) {
constructor(contentManagementClient: ContentClient) {
this.contentManagementClient = contentManagementClient;
this.savedObjectClient = savedObjectClient;
}
async find(options: SavedObjectsClientCommonFindArgs) {
@ -75,15 +70,6 @@ export class SavedObjectsClientPublicToCommon implements SavedObjectsClientCommo
return response.item;
}
async getSavedSearch(id: string) {
const response = await this.savedObjectClient.resolve('search', id);
if (response.outcome === 'conflict') {
throw new DataViewSavedObjectConflictError(id);
}
return response.saved_object;
}
async update(
id: string,
attributes: DataViewAttributes,

View file

@ -19,7 +19,7 @@ export type {
DataViewSpec,
FieldSpec,
DataViewAttributes,
SavedObjectsClientCommon,
PersistenceAPI,
RuntimeField,
} from '../common';
export {
@ -48,7 +48,6 @@ export type {
export { DataViewsApiClient, DataViewsService, DataView } from './data_views';
export type { DataViewListItem } from './data_views';
export { UiSettingsPublicToCommon } from './ui_settings_wrapper';
export { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper';
/*
* Plugin setup

View file

@ -17,7 +17,7 @@ import {
} from './types';
import { DataViewsApiClient } from '.';
import { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper';
import { ContentMagementWrapper } from './content_management_wrapper';
import { UiSettingsPublicToCommon } from './ui_settings_wrapper';
@ -63,7 +63,7 @@ export class DataViewsPublicPlugin
core: CoreStart,
{ fieldFormats, contentManagement }: DataViewsPublicStartDependencies
): DataViewsPublicPluginStart {
const { uiSettings, http, notifications, application, savedObjects } = core;
const { uiSettings, http, notifications, application } = core;
const onNotifDebounced = debounceByKey(
notifications.toasts.add.bind(notifications.toasts),
@ -77,10 +77,7 @@ export class DataViewsPublicPlugin
return new DataViewsServicePublic({
hasData: this.hasData.start(core),
uiSettings: new UiSettingsPublicToCommon(uiSettings),
savedObjectsClient: new SavedObjectsClientPublicToCommon(
contentManagement.client,
savedObjects.client
),
savedObjectsClient: new ContentMagementWrapper(contentManagement.client),
apiClient: new DataViewsApiClient(http),
fieldFormats,
onNotification: (toastInputFields, key) => {

View file

@ -18,7 +18,7 @@ import { FieldFormatsStart } from '@kbn/field-formats-plugin/server';
import { DataViewsService } from '../common';
import { UiSettingsServerToCommon } from './ui_settings_wrapper';
import { IndexPatternsApiServer } from './index_patterns_api_client';
import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper';
import { SavedObjectsClientWrapper } from './saved_objects_client_wrapper';
interface DataViewsServiceFactoryDeps {
logger: Logger;
@ -44,7 +44,7 @@ export const dataViewsServiceFactory = (deps: DataViewsServiceFactoryDeps) =>
return new DataViewsService({
uiSettings: new UiSettingsServerToCommon(uiSettingsClient),
savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient),
savedObjectsClient: new SavedObjectsClientWrapper(savedObjectsClient),
apiClient: new IndexPatternsApiServer(elasticsearchClient, savedObjectsClient),
fieldFormats: formats,
onError: (error) => {

View file

@ -57,5 +57,5 @@ export {
export type { SERVICE_KEY_TYPE } from './constants';
export type { FieldSpec, SavedObjectsClientCommon } from '../common/types';
export type { FieldSpec } from '../common/types';
export { DataViewsService, DataView } from '../common/data_views';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper';
import { SavedObjectsClientWrapper } from './saved_objects_client_wrapper';
import { SavedObjectsClientContract } from '@kbn/core/server';
import { DataViewSavedObjectConflictError } from '../common';
@ -21,7 +21,7 @@ describe('SavedObjectsClientPublicToCommon', () => {
soClient.resolve = jest
.fn()
.mockResolvedValue({ outcome: 'exactMatch', saved_object: mockedSavedObject });
const service = new SavedObjectsClientServerToCommon(soClient);
const service = new SavedObjectsClientWrapper(soClient);
const result = await service.get('1');
expect(result).toStrictEqual(mockedSavedObject);
});
@ -33,7 +33,7 @@ describe('SavedObjectsClientPublicToCommon', () => {
soClient.resolve = jest
.fn()
.mockResolvedValue({ outcome: 'aliasMatch', saved_object: mockedSavedObject });
const service = new SavedObjectsClientServerToCommon(soClient);
const service = new SavedObjectsClientWrapper(soClient);
const result = await service.get('1');
expect(result).toStrictEqual(mockedSavedObject);
});
@ -46,7 +46,7 @@ describe('SavedObjectsClientPublicToCommon', () => {
soClient.resolve = jest
.fn()
.mockResolvedValue({ outcome: 'conflict', saved_object: mockedSavedObject });
const service = new SavedObjectsClientServerToCommon(soClient);
const service = new SavedObjectsClientWrapper(soClient);
await expect(service.get('1')).rejects.toThrow(DataViewSavedObjectConflictError);
});

View file

@ -9,7 +9,7 @@
import { SavedObjectsClientContract, SavedObject } from '@kbn/core/server';
import {
DataViewAttributes,
SavedObjectsClientCommon,
PersistenceAPI,
SavedObjectsClientCommonFindArgs,
} from '../common/types';
import { DataViewSavedObjectConflictError } from '../common/errors';
@ -17,7 +17,7 @@ import { DataViewSavedObjectConflictError } from '../common/errors';
import type { DataViewCrudTypes } from '../common/content_management';
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../common';
export class SavedObjectsClientServerToCommon implements SavedObjectsClientCommon {
export class SavedObjectsClientWrapper implements PersistenceAPI {
private savedObjectClient: SavedObjectsClientContract;
constructor(savedObjectClient: SavedObjectsClientContract) {
this.savedObjectClient = savedObjectClient;

View file

@ -0,0 +1,9 @@
/*
* 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 { kibanaContext } from './kibana_context_type';

View file

@ -9,17 +9,16 @@
import { FilterStateStore, buildFilter, FILTERS } from '@kbn/es-query';
import type { DeeplyMockedKeys } from '@kbn/utility-types-jest';
import type { ExecutionContext } from '@kbn/expressions-plugin/common';
import { KibanaContext } from './kibana_context_type';
import { KibanaContext, ExpressionFunctionKibanaContext } from '@kbn/data-plugin/common';
import { fromSavedSearchAttributes } from '../service/saved_searches_utils';
import type { SavedSearchAttributes, SavedSearch } from '../types';
import {
getKibanaContextFn,
ExpressionFunctionKibanaContext,
KibanaContextStartDependencies,
} from './kibana_context';
import { getKibanaContextFn, KibanaContextStartDependencies } from './kibana_context';
type StartServicesMock = DeeplyMockedKeys<KibanaContextStartDependencies>;
const createExecutionContextMock = (): DeeplyMockedKeys<ExecutionContext> => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
abortSignal: {} as any,
getExecutionContext: jest.fn(),
getSearchContext: jest.fn(),
@ -41,23 +40,26 @@ describe('kibanaContextFn', () => {
beforeEach(async () => {
kibanaContextFn = getKibanaContextFn(getStartServicesMock);
startServicesMock = {
savedObjectsClient: {
create: jest.fn(),
delete: jest.fn(),
find: jest.fn(),
get: jest.fn(),
getSavedSearch: jest.fn(),
update: jest.fn(),
},
getSavedSearch: jest.fn(),
};
});
it('merges and deduplicates queries from different sources', async () => {
const { fn } = kibanaContextFn;
startServicesMock.savedObjectsClient.getSavedSearch.mockResolvedValue({
attributes: {
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
startServicesMock.getSavedSearch.mockResolvedValue(
fromSavedSearchAttributes(
'abc',
{
kibanaSavedObjectMeta: {
searchSourceJSON: JSON.stringify({
query: [],
}),
},
} as SavedSearchAttributes,
[],
undefined,
{
getFields: () => ({
query: [
{
language: 'kuery',
@ -84,10 +86,12 @@ describe('kibanaContextFn', () => {
},
},
],
filter: [],
}),
},
},
} as any);
} as unknown as SavedSearch['searchSource'],
{} as SavedSearch['sharingSavedObjectProps']
)
);
const args = {
...emptyArgs,
q: [

View file

@ -0,0 +1,139 @@
/*
* 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 { isEqual, uniqBy } from 'lodash';
import { i18n } from '@kbn/i18n';
import { ExecutionContext } from '@kbn/expressions-plugin/common';
import { Filter, fromCombinedFilter } from '@kbn/es-query';
import { Query, uniqFilters } from '@kbn/es-query';
import { unboxExpressionValue } from '@kbn/expressions-plugin/common';
import { SavedObjectReference } from '@kbn/core/server';
import { ExpressionFunctionKibanaContext } from '@kbn/data-plugin/common';
import { SavedSearch } from '../types';
export interface KibanaContextStartDependencies {
getSavedSearch: (id: string) => Promise<SavedSearch>;
}
const mergeQueries = (first: Query | Query[] = [], second: Query | Query[]) =>
uniqBy<Query>(
[...(Array.isArray(first) ? first : [first]), ...(Array.isArray(second) ? second : [second])],
(n) => JSON.stringify(n.query)
);
export const getKibanaContextFn = (
getStartDependencies: (
getKibanaRequest: ExecutionContext['getKibanaRequest']
) => Promise<KibanaContextStartDependencies>
) => {
const kibanaContextFunction: ExpressionFunctionKibanaContext = {
name: 'kibana_context',
type: 'kibana_context',
inputTypes: ['kibana_context', 'null'],
help: i18n.translate('savedSearch.kibana_context.help', {
defaultMessage: 'Updates kibana global context',
}),
args: {
q: {
types: ['kibana_query', 'null'],
multi: true,
aliases: ['query', '_'],
help: i18n.translate('savedSearch.kibana_context.q.help', {
defaultMessage: 'Specify Kibana free form text query',
}),
},
filters: {
types: ['kibana_filter', 'null'],
multi: true,
help: i18n.translate('savedSearch.kibana_context.filters.help', {
defaultMessage: 'Specify Kibana generic filters',
}),
},
timeRange: {
types: ['timerange', 'null'],
default: null,
help: i18n.translate('savedSearch.kibana_context.timeRange.help', {
defaultMessage: 'Specify Kibana time range filter',
}),
},
savedSearchId: {
types: ['string', 'null'],
default: null,
help: i18n.translate('savedSearch.kibana_context.savedSearchId.help', {
defaultMessage: 'Specify saved search ID to be used for queries and filters',
}),
},
},
extract(state) {
const references: SavedObjectReference[] = [];
if (state.savedSearchId.length && typeof state.savedSearchId[0] === 'string') {
const refName = 'kibana_context.savedSearchId';
references.push({
name: refName,
type: 'search',
id: state.savedSearchId[0] as string,
});
return {
state: {
...state,
savedSearchId: [refName],
},
references,
};
}
return { state, references };
},
inject(state, references) {
const reference = references.find((r) => r.name === 'kibana_context.savedSearchId');
if (reference) {
state.savedSearchId[0] = reference.id;
}
return state;
},
async fn(input, args, { getKibanaRequest }) {
const { getSavedSearch } = await getStartDependencies(getKibanaRequest);
const timeRange = args.timeRange || input?.timeRange;
let queries = mergeQueries(input?.query, args?.q?.filter(Boolean) || []);
const filterFromArgs = (args?.filters?.map(unboxExpressionValue) || []) as Filter[];
let filters = [...(input?.filters || [])];
if (args.savedSearchId) {
const obj = await getSavedSearch(args.savedSearchId);
const { query, filter } = obj.searchSource.getFields();
if (query) {
queries = mergeQueries(queries, query as Query);
}
if (filter) {
filters = [...filters, ...(Array.isArray(filter) ? filter : [filter])] as Filter[];
}
}
const uniqueArgFilters = filterFromArgs.filter(
(argF) =>
!filters.some((f) => {
return isEqual(fromCombinedFilter(f).query, argF.query);
})
);
filters = [...filters, ...uniqueArgFilters];
return {
type: 'kibana_context',
query: queries,
filters: uniqFilters(filters.filter((f: Filter) => !f.meta?.disabled)),
timeRange,
};
},
};
return kibanaContextFunction;
};

View file

@ -0,0 +1,36 @@
/*
* 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 { ExpressionValueFilter } from '@kbn/expressions-plugin/common';
import { adaptToExpressionValueFilter, KibanaContext } from '@kbn/data-plugin/common';
export const kibanaContext = {
name: 'kibana_context',
from: {
null: () => {
return {
type: 'kibana_context',
};
},
},
to: {
null: () => {
return {
type: 'null',
};
},
filter: (input: KibanaContext): ExpressionValueFilter => {
const { filters = [] } = input;
return {
type: 'filter',
filterType: 'filter',
and: filters.map(adaptToExpressionValueFilter),
};
},
},
};

View file

@ -23,3 +23,4 @@ export enum VIEW_MODE {
export { SavedSearchType } from './constants';
export { LATEST_VERSION } from './constants';
export { getKibanaContextFn } from './expressions/kibana_context';

View file

@ -6,26 +6,25 @@
* Side Public License, v 1.
*/
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks';
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { getSavedSearch } from './get_saved_searches';
import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { GetSavedSearchDependencies } from './get_saved_searches';
describe('getSavedSearch', () => {
let search: DataPublicPluginStart['search'];
let cmClient: ContentManagementPublicStart['client'];
let searchSourceCreate: DataPublicPluginStart['search']['searchSource']['create'];
let getSavedSrch: GetSavedSearchDependencies['getSavedSrch'];
beforeEach(() => {
cmClient = contentManagementMock.createStartContract().client;
search = dataPluginMock.createStartContract().search;
getSavedSrch = jest.fn();
searchSourceCreate = dataPluginMock.createStartContract().search.searchSource.create;
});
test('should throw an error if so not found', async () => {
let errorMessage = 'No error thrown.';
cmClient.get = jest.fn().mockReturnValue({
getSavedSrch = jest.fn().mockReturnValue({
statusCode: 404,
error: 'Not Found',
message: 'Saved object [ccf1af80-2297-11ec-86e0-1155ffb9c7a7] not found',
@ -33,8 +32,8 @@ describe('getSavedSearch', () => {
try {
await getSavedSearch('ccf1af80-2297-11ec-86e0-1155ffb9c7a7', {
contentManagement: cmClient,
search,
getSavedSrch,
searchSourceCreate,
});
} catch (error) {
errorMessage = error.message;
@ -46,7 +45,7 @@ describe('getSavedSearch', () => {
});
test('should find saved search', async () => {
cmClient.get = jest.fn().mockReturnValue({
getSavedSrch = jest.fn().mockReturnValue({
item: {
attributes: {
kibanaSavedObjectMeta: {
@ -77,11 +76,11 @@ describe('getSavedSearch', () => {
});
const savedSearch = await getSavedSearch('ccf1af80-2297-11ec-86e0-1155ffb9c7a7', {
contentManagement: cmClient,
search,
getSavedSrch,
searchSourceCreate,
});
expect(cmClient.get).toHaveBeenCalled();
expect(getSavedSrch).toHaveBeenCalled();
expect(savedSearch).toMatchInlineSnapshot(`
Object {
"breakdownField": undefined,
@ -150,7 +149,7 @@ describe('getSavedSearch', () => {
});
test('should find saved search with sql mode', async () => {
cmClient.get = jest.fn().mockReturnValue({
getSavedSrch = jest.fn().mockReturnValue({
item: {
attributes: {
kibanaSavedObjectMeta: {
@ -182,11 +181,11 @@ describe('getSavedSearch', () => {
});
const savedSearch = await getSavedSearch('ccf1af80-2297-11ec-86e0-1155ffb9c7a7', {
contentManagement: cmClient,
search,
getSavedSrch,
searchSourceCreate,
});
expect(cmClient.get).toHaveBeenCalled();
expect(getSavedSrch).toHaveBeenCalled();
expect(savedSearch).toMatchInlineSnapshot(`
Object {
"breakdownField": undefined,
@ -255,7 +254,7 @@ describe('getSavedSearch', () => {
});
it('should call savedObjectsTagging.ui.getTagIdsFromReferences', async () => {
cmClient.get = jest.fn().mockReturnValue({
getSavedSrch = jest.fn().mockReturnValue({
item: {
attributes: {
kibanaSavedObjectMeta: {
@ -296,8 +295,8 @@ describe('getSavedSearch', () => {
},
} as unknown as SavedObjectsTaggingApi;
await getSavedSearch('ccf1af80-2297-11ec-86e0-1155ffb9c7a7', {
contentManagement: cmClient,
search,
getSavedSrch,
searchSourceCreate,
savedObjectsTagging,
});
expect(savedObjectsTagging.ui.getTagIdsFromReferences).toHaveBeenCalledWith([

View file

@ -6,20 +6,21 @@
* Side Public License, v 1.
*/
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { injectSearchSourceReferences, parseSearchSourceJSON } from '@kbn/data-plugin/public';
import type { ISearchStartSearchSource } from '@kbn/data-plugin/common';
import { injectReferences, parseSearchSourceJSON } from '@kbn/data-plugin/common';
// these won't exist in on server
import type { SpacesApi } from '@kbn/spaces-plugin/public';
import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import { i18n } from '@kbn/i18n';
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import type { SavedSearch } from './types';
import { SAVED_SEARCH_TYPE } from './constants';
import { fromSavedSearchAttributes } from './saved_searches_utils';
import type { SavedSearchCrudTypes } from '../../../common/content_management';
interface GetSavedSearchDependencies {
search: DataPublicPluginStart['search'];
contentManagement: ContentManagementPublicStart['client'];
import { i18n } from '@kbn/i18n';
import type { SavedSearch } from '../types';
import { SavedSearchType as SAVED_SEARCH_TYPE } from '..';
import { fromSavedSearchAttributes } from './saved_searches_utils';
import type { SavedSearchCrudTypes } from '../content_management';
export interface GetSavedSearchDependencies {
searchSourceCreate: ISearchStartSearchSource['create'];
getSavedSrch: (id: string) => Promise<SavedSearchCrudTypes['GetOut']>;
spaces?: SpacesApi;
savedObjectsTagging?: SavedObjectsTaggingApi;
}
@ -32,15 +33,9 @@ const getSavedSearchUrlConflictMessage = async (json: string) =>
export const getSavedSearch = async (
savedSearchId: string,
{ search, spaces, savedObjectsTagging, contentManagement }: GetSavedSearchDependencies
{ searchSourceCreate, spaces, savedObjectsTagging, getSavedSrch }: GetSavedSearchDependencies
) => {
const so = await contentManagement.get<
SavedSearchCrudTypes['GetIn'],
SavedSearchCrudTypes['GetOut']
>({
contentTypeId: SAVED_SEARCH_TYPE,
id: savedSearchId,
});
const so = await getSavedSrch(savedSearchId);
// @ts-expect-error
if (so.error) {
@ -53,6 +48,7 @@ export const getSavedSearch = async (
JSON.stringify({
targetType: SAVED_SEARCH_TYPE,
sourceId: savedSearchId,
// front end only
targetSpace: (await spaces?.getActiveSpace())?.id,
})
)
@ -65,11 +61,12 @@ export const getSavedSearch = async (
savedSearch.attributes.kibanaSavedObjectMeta?.searchSourceJSON ?? '{}'
);
const searchSourceValues = injectSearchSourceReferences(
parsedSearchSourceJSON as Parameters<typeof injectSearchSourceReferences>[0],
const searchSourceValues = injectReferences(
parsedSearchSourceJSON as Parameters<typeof injectReferences>[0],
savedSearch.references
);
// front end only
const tags = savedObjectsTagging
? savedObjectsTagging.ui.getTagIdsFromReferences(savedSearch.references)
: undefined;
@ -79,7 +76,7 @@ export const getSavedSearch = async (
savedSearch.attributes,
tags,
savedSearch.references,
await search.searchSource.create(searchSourceValues),
await searchSourceCreate(searchSourceValues),
so.meta
);
@ -92,9 +89,9 @@ export const getSavedSearch = async (
* @param search
*/
export const getNewSavedSearch = ({
search,
searchSource,
}: {
search: DataPublicPluginStart['search'];
searchSource: ISearchStartSearchSource;
}): SavedSearch => ({
searchSource: search.searchSource.createEmpty(),
searchSource: searchSource.createEmpty(),
});

View file

@ -10,8 +10,7 @@ import { fromSavedSearchAttributes, toSavedSearchAttributes } from './saved_sear
import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks';
import type { SavedSearchAttributes } from '../../../common';
import type { SavedSearch } from './types';
import type { SavedSearch, SavedSearchAttributes } from '../types';
describe('saved_searches_utils', () => {
describe('fromSavedSearchAttributes', () => {

View file

@ -8,11 +8,10 @@
import { pick } from 'lodash';
import type { SavedObjectReference } from '@kbn/core-saved-objects-server';
import type { SavedSearchAttributes } from '../../../common';
import { fromSavedSearchAttributes as fromSavedSearchAttributesCommon } from '../../../common';
import type { SavedSearch } from './types';
import type { SavedSearchAttributes, SavedSearch } from '..';
import { fromSavedSearchAttributes as fromSavedSearchAttributesCommon } from '..';
export { getSavedSearchUrl, getSavedSearchFullPathUrl } from '../../../common';
export { getSavedSearchUrl, getSavedSearchFullPathUrl } from '..';
export const fromSavedSearchAttributes = (
id: string,

View file

@ -8,6 +8,7 @@
import type { ISearchSource, RefreshInterval, TimeRange } from '@kbn/data-plugin/common';
import type { SavedObjectReference } from '@kbn/core-saved-objects-server';
import type { SavedObjectsResolveResponse } from '@kbn/core/server';
import { VIEW_MODE } from '.';
export interface DiscoverGridSettings {
@ -75,4 +76,10 @@ export interface SavedSearch {
rowsPerPage?: number;
breakdownField?: string;
references?: SavedObjectReference[];
sharingSavedObjectProps?: {
outcome?: SavedObjectsResolveResponse['outcome'];
aliasTargetId?: SavedObjectsResolveResponse['alias_target_id'];
aliasPurpose?: SavedObjectsResolveResponse['alias_purpose'];
errorJSON?: string;
};
}

View file

@ -9,7 +9,8 @@
"browser": true,
"requiredPlugins": [
"data",
"contentManagement"
"contentManagement",
"expressions"
],
"optionalPlugins": [
"spaces",

View file

@ -7,9 +7,8 @@
*/
import { StartServicesAccessor } from '@kbn/core/public';
import { SavedObjectsClientCommon } from '@kbn/data-views-plugin/public';
import { getKibanaContextFn } from '../../../common/search/expressions';
import { DataPublicPluginStart, DataStartDependencies } from '../../types';
import { getKibanaContextFn } from '../../common';
import { SavedSearchPublicPluginStart, SavedSearchPublicStartDependencies } from '../plugin';
/**
* This is some glue code that takes in `core.getStartServices`, extracts the dependencies
@ -25,15 +24,17 @@ import { DataPublicPluginStart, DataStartDependencies } from '../../types';
*
* @internal
*/
export function getKibanaContext({
getStartServices,
}: {
getStartServices: StartServicesAccessor<DataStartDependencies, DataPublicPluginStart>;
getStartServices: StartServicesAccessor<
SavedSearchPublicStartDependencies,
SavedSearchPublicPluginStart
>;
}) {
return getKibanaContextFn(async () => {
const [core] = await getStartServices();
return {
savedObjectsClient: core.savedObjects.client as unknown as SavedObjectsClientCommon,
};
const [, , { get: getSavedSearch }] = await getStartServices();
return { getSavedSearch };
});
}

View file

@ -6,10 +6,11 @@
* Side Public License, v 1.
*/
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { CoreSetup, CoreStart, Plugin, StartServicesAccessor } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { SpacesApi } from '@kbn/spaces-plugin/public';
import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public';
import { ExpressionsSetup } from '@kbn/expressions-plugin/public';
import { i18n } from '@kbn/i18n';
import type {
ContentManagementPublicSetup,
@ -25,6 +26,8 @@ import {
import { SavedSearch, SavedSearchAttributes } from '../common/types';
import { SavedSearchType, LATEST_VERSION } from '../common';
import { SavedSearchesService } from './services/saved_searches/saved_searches_service';
import { kibanaContext } from '../common/expressions';
import { getKibanaContext } from './expressions/kibana_context';
/**
* Saved search plugin public Setup contract
@ -50,6 +53,7 @@ export interface SavedSearchPublicPluginStart {
*/
export interface SavedSearchPublicSetupDependencies {
contentManagement: ContentManagementPublicSetup;
expressions: ExpressionsSetup;
}
/**
@ -71,7 +75,10 @@ export class SavedSearchPublicPlugin
SavedSearchPublicStartDependencies
>
{
public setup(core: CoreSetup, { contentManagement }: SavedSearchPublicSetupDependencies) {
public setup(
{ getStartServices }: CoreSetup,
{ contentManagement, expressions }: SavedSearchPublicSetupDependencies
) {
contentManagement.registry.register({
id: SavedSearchType,
version: {
@ -82,6 +89,17 @@ export class SavedSearchPublicPlugin
}),
});
expressions.registerFunction(
getKibanaContext({ getStartServices } as {
getStartServices: StartServicesAccessor<
SavedSearchPublicStartDependencies,
SavedSearchPublicPluginStart
>;
})
);
expressions.registerType(kibanaContext);
return {};
}

View file

@ -6,8 +6,11 @@
* Side Public License, v 1.
*/
export { getSavedSearch, getNewSavedSearch } from './get_saved_searches';
export { getSavedSearchUrl, getSavedSearchFullPathUrl } from './saved_searches_utils';
export { getSavedSearch, getNewSavedSearch } from '../../../common/service/get_saved_searches';
export {
getSavedSearchUrl,
getSavedSearchFullPathUrl,
} from '../../../common/service/saved_searches_utils';
export type { SaveSavedSearchOptions } from './save_saved_searches';
export { saveSavedSearch } from './save_saved_searches';
export { SAVED_SEARCH_TYPE } from './constants';

View file

@ -10,7 +10,7 @@ import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plug
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import type { SavedSearch } from './types';
import { SAVED_SEARCH_TYPE } from './constants';
import { toSavedSearchAttributes } from './saved_searches_utils';
import { toSavedSearchAttributes } from '../../../common/service/saved_searches_utils';
import type { SavedSearchCrudTypes } from '../../../common/content_management';
export interface SaveSavedSearchOptions {

View file

@ -27,10 +27,16 @@ export class SavedSearchesService {
get = (savedSearchId: string) => {
const { search, contentManagement, spaces, savedObjectsTaggingOss } = this.deps;
const getViaCm = (id: string) =>
contentManagement.get<SavedSearchCrudTypes['GetIn'], SavedSearchCrudTypes['GetOut']>({
contentTypeId: SavedSearchType,
id,
});
return getSavedSearch(savedSearchId, {
search,
contentManagement,
getSavedSrch: getViaCm,
spaces,
searchSourceCreate: search.searchSource.create,
savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(),
});
};
@ -45,7 +51,7 @@ export class SavedSearchesService {
});
return result.hits;
};
getNew = () => getNewSavedSearch({ search: this.deps.search });
getNew = () => getNewSavedSearch({ searchSource: this.deps.search.searchSource });
find = async (search: string) => {
const { contentManagement } = this.deps;

View file

@ -7,9 +7,10 @@
*/
import { StartServicesAccessor } from '@kbn/core/server';
import { SavedObjectsClientCommon } from '@kbn/data-views-plugin/server';
import { getKibanaContextFn } from '../../../common/search/expressions';
import { DataPluginStart, DataPluginStartDependencies } from '../../plugin';
import { getKibanaContextFn } from '../../common';
import { SavedSearchServerStartDeps } from '../plugin';
import { getSavedSearch } from '../../common/service/get_saved_searches';
import { SavedSearchAttributes } from '../../common/types';
/**
* This is some glue code that takes in `core.getStartServices`, extracts the dependencies
@ -25,20 +26,36 @@ import { DataPluginStart, DataPluginStartDependencies } from '../../plugin';
*
* @internal
*/
export function getKibanaContext({
getStartServices,
}: {
getStartServices: StartServicesAccessor<DataPluginStartDependencies, DataPluginStart>;
}) {
export function getKibanaContext(
getStartServices: StartServicesAccessor<SavedSearchServerStartDeps>
) {
return getKibanaContextFn(async (getKibanaRequest) => {
const request = getKibanaRequest && getKibanaRequest();
if (!request) {
throw new Error('KIBANA_CONTEXT_KIBANA_REQUEST_MISSING');
}
const [{ savedObjects }] = await getStartServices();
const [{ savedObjects }, { data }] = await getStartServices();
return {
savedObjectsClient: savedObjects.getScopedClient(request) as any as SavedObjectsClientCommon,
getSavedSearch: async (id: string) => {
const searchSourceCreate = (await data.search.searchSource.asScoped(request)).create;
const getSavedSrch = async (searchId: string) => {
const so = await savedObjects
.getScopedClient(request)
.resolve<SavedSearchAttributes>('search', searchId);
return {
item: so.saved_object,
meta: {
outcome: so.outcome,
aliasTargetId: so.alias_target_id,
aliasPurpose: so.alias_purpose,
},
};
};
return getSavedSearch(id, { searchSourceCreate, getSavedSrch });
},
};
});
}

View file

@ -7,21 +7,41 @@
*/
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
import type { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server';
import { StartServicesAccessor } from '@kbn/core/server';
import type {
PluginSetup as DataPluginSetup,
PluginStart as DataPluginStart,
} from '@kbn/data-plugin/server';
import type { ContentManagementServerSetup } from '@kbn/content-management-plugin/server';
import { ExpressionsServerSetup } from '@kbn/expressions-plugin/server';
import { getSavedSearchObjectType } from './saved_objects';
import { SavedSearchType, LATEST_VERSION } from '../common';
import { SavedSearchStorage } from './content_management';
import { kibanaContext } from '../common/expressions';
import { getKibanaContext } from './expressions/kibana_context';
import { getSavedSearch } from '../common/service/get_saved_searches';
export class SavedSearchServerPlugin implements Plugin<object, object> {
/**
* Saved search plugin server Setup contract
*/
export interface SavedSearchPublicSetupDependencies {
data: DataPluginSetup;
contentManagement: ContentManagementServerSetup;
expressions: ExpressionsServerSetup;
}
export interface SavedSearchServerStartDeps {
data: DataPluginStart;
}
export class SavedSearchServerPlugin
implements Plugin<object, object, object, SavedSearchServerStartDeps>
{
public setup(
core: CoreSetup,
plugins: {
data: DataPluginSetup;
contentManagement: ContentManagementServerSetup;
}
{ data, contentManagement, expressions }: SavedSearchPublicSetupDependencies
) {
plugins.contentManagement.register({
contentManagement.register({
id: SavedSearchType,
storage: new SavedSearchStorage(),
version: {
@ -29,16 +49,23 @@ export class SavedSearchServerPlugin implements Plugin<object, object> {
},
});
const getSearchSourceMigrations = plugins.data.search.searchSource.getAllMigrations.bind(
plugins.data.search.searchSource
);
const searchSource = data.search.searchSource;
const getSearchSourceMigrations = searchSource.getAllMigrations.bind(searchSource);
core.savedObjects.registerType(getSavedSearchObjectType(getSearchSourceMigrations));
expressions.registerType(kibanaContext);
expressions.registerFunction(
getKibanaContext(core.getStartServices as StartServicesAccessor<SavedSearchServerStartDeps>)
);
return {};
}
public start(core: CoreStart) {
return {};
return {
getSavedSearch,
};
}
public stop() {}

View file

@ -22,6 +22,9 @@
"@kbn/object-versioning",
"@kbn/content-management-utils",
"@kbn/content-management-plugin",
"@kbn/es-query",
"@kbn/utility-types-jest",
"@kbn/expressions-plugin",
],
"exclude": [
"target/**/*",

View file

@ -0,0 +1,27 @@
{
"attributes": {
"columns": [
"_source"
],
"description": "A Saved Search Description",
"hits": 0,
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"geo.src :\\\"FR\\\" \",\"language\":\"kuery\"},\"filter\":[]}"
},
"sort": [],
"title": "A Saved Search",
"version": 1,
"timeRange" : {
"from": "2006-09-21T00:00:00Z",
"to": "2015-09-22T00:00:00Z"
}
},
"coreMigrationVersion": "7.17.1",
"id": "ab12e3c0-f231-11e6-9486-733b1ac9221a",
"migrationVersion": {
"search": "7.9.3"
},
"references": [],
"type": "search",
"version": "WzQzLDJd"
}

View file

@ -23,6 +23,7 @@ export default function ({
updateBaselines,
}: FtrProviderContext & { updateBaselines: boolean }) {
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
let expectExpression: ExpectExpression;
const expectClientToMatchServer = async (title: string, expression: string) => {
@ -92,6 +93,43 @@ export default function ({
});
});
describe('loads a saved search', () => {
before(async () => {
await kibanaServer.importExport.load(
'test/functional/fixtures/kbn_archiver/saved_search.json'
);
});
after(async () => {
await kibanaServer.importExport.unload(
'test/functional/fixtures/kbn_archiver/saved_search.json'
);
});
const expression = `
kibana_context savedSearchId="ab12e3c0-f231-11e6-9486-733b1ac9221a"
| esaggs index={indexPatternLoad id='logstash-*'}
aggs={aggCount id="1" enabled=true schema="metric"}
`;
it('correctly applies filter from saved search', async () => {
const result = await expectExpression('esaggs_saved_searches', expression).getResponse();
expect(getCell(result, 0, 0)).to.be(119);
});
it('correctly applies filter - on the server', async () => {
await supertest
.post('/api/interpreter_functional/run_expression')
.set('kbn-xsrf', 'anything')
.send({ expression, input: undefined })
.expect(200)
.expect(({ body }) => {
expect(body.columns[0].meta.index).to.be('logstash-*');
expect(body.columns[0].meta.source).to.be('esaggs');
expect(getCell(body, 0, 0)).to.be(119);
});
});
});
describe('correctly runs on the server', () => {
it('runs the provided agg on the server', async () => {
const expression = `

View file

@ -8,7 +8,7 @@
import type { IUiSettingsClient } from '@kbn/core/public';
import { DataView } from '@kbn/data-views-plugin/common';
import { createSearchItems } from './new_job_utils';
import { fromSavedSearchAttributes } from '@kbn/saved-search-plugin/public/services/saved_searches/saved_searches_utils';
import { fromSavedSearchAttributes } from '@kbn/saved-search-plugin/common';
import type { ISearchSource } from '@kbn/data-plugin/public';
describe('createSearchItems', () => {
@ -42,17 +42,9 @@ describe('createSearchItems', () => {
isTextBasedQuery: false,
},
[],
[
{
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
type: 'index-pattern',
id: '7e252840-bd27-11ea-8a6c-75d1a0bd08ab',
},
],
{
getField: getFieldMock(searchSource),
} as unknown as ISearchSource,
{}
} as unknown as ISearchSource
);
test('should match data view', () => {

View file

@ -2004,11 +2004,11 @@
"data.search.functions.ipRange.from.help": "Spécifier l'adresse de début",
"data.search.functions.ipRange.help": "Créer une plage d'IP",
"data.search.functions.ipRange.to.help": "Spécifier l'adresse de fin",
"data.search.functions.kibana_context.filters.help": "Spécifier des filtres génériques Kibana",
"data.search.functions.kibana_context.help": "Met à jour le contexte général de Kibana.",
"data.search.functions.kibana_context.q.help": "Spécifier une recherche en texte libre Kibana",
"data.search.functions.kibana_context.savedSearchId.help": "Spécifier l'ID de recherche enregistrée à utiliser pour les requêtes et les filtres",
"data.search.functions.kibana_context.timeRange.help": "Spécifier le filtre de plage temporelle Kibana",
"savedSearch.kibana_context.filters.help": "Spécifier des filtres génériques Kibana",
"savedSearch.kibana_context.help": "Met à jour le contexte général de Kibana.",
"savedSearch.kibana_context.q.help": "Spécifier une recherche en texte libre Kibana",
"savedSearch.kibana_context.savedSearchId.help": "Spécifier l'ID de recherche enregistrée à utiliser pour les requêtes et les filtres",
"savedSearch.kibana_context.timeRange.help": "Spécifier le filtre de plage temporelle Kibana",
"data.search.functions.kibana.help": "Permet dobtenir le contexte général de Kibana.",
"data.search.functions.kibanaFilter.disabled.help": "Si le filtre doit être désactivé",
"data.search.functions.kibanaFilter.field.help": "Spécifier une recherche en texte libre esdsl",

View file

@ -2004,11 +2004,11 @@
"data.search.functions.ipRange.from.help": "開始アドレスを指定",
"data.search.functions.ipRange.help": "IP範囲を作成",
"data.search.functions.ipRange.to.help": "終了アドレスを指定",
"data.search.functions.kibana_context.filters.help": "Kibana ジェネリックフィルターを指定します",
"data.search.functions.kibana_context.help": "Kibana グローバルコンテキストを更新します",
"data.search.functions.kibana_context.q.help": "自由形式の Kibana テキストクエリを指定します",
"data.search.functions.kibana_context.savedSearchId.help": "クエリとフィルターに使用する保存検索ID を指定します。",
"data.search.functions.kibana_context.timeRange.help": "Kibana 時間範囲フィルターを指定します",
"savedSearch.kibana_context.filters.help": "Kibana ジェネリックフィルターを指定します",
"savedSearch.kibana_context.help": "Kibana グローバルコンテキストを更新します",
"savedSearch.kibana_context.q.help": "自由形式の Kibana テキストクエリを指定します",
"savedSearch.kibana_context.savedSearchId.help": "クエリとフィルターに使用する保存検索ID を指定します。",
"savedSearch.kibana_context.timeRange.help": "Kibana 時間範囲フィルターを指定します",
"data.search.functions.kibana.help": "Kibana グローバルコンテキストを取得します",
"data.search.functions.kibanaFilter.disabled.help": "フィルターは無効でなければなりません",
"data.search.functions.kibanaFilter.field.help": "フリーフォームesdslクエリを指定",

View file

@ -2004,11 +2004,11 @@
"data.search.functions.ipRange.from.help": "指定开始地址",
"data.search.functions.ipRange.help": "创建 IP 范围",
"data.search.functions.ipRange.to.help": "指定结束地址",
"data.search.functions.kibana_context.filters.help": "指定 Kibana 常规筛选",
"data.search.functions.kibana_context.help": "更新 kibana 全局上下文",
"data.search.functions.kibana_context.q.help": "指定 Kibana 自由格式文本查询",
"data.search.functions.kibana_context.savedSearchId.help": "指定要用于查询和筛选的已保存搜索 ID",
"data.search.functions.kibana_context.timeRange.help": "指定 Kibana 时间范围筛选",
"savedSearch.kibana_context.filters.help": "指定 Kibana 常规筛选",
"savedSearch.kibana_context.help": "更新 kibana 全局上下文",
"savedSearch.kibana_context.q.help": "指定 Kibana 自由格式文本查询",
"savedSearch.kibana_context.savedSearchId.help": "指定要用于查询和筛选的已保存搜索 ID",
"savedSearch.kibana_context.timeRange.help": "指定 Kibana 时间范围筛选",
"data.search.functions.kibana.help": "获取 kibana 全局上下文",
"data.search.functions.kibanaFilter.disabled.help": "如果禁用该筛选",
"data.search.functions.kibanaFilter.field.help": "指定自由格式 esdsl 查询",