Create Extensions of DiscoverAppLocator for DiscoverServerPlugin (#150631)

## Summary

Adds `DiscoverServerPluginLocatorService`, a set of utilities that
allows consumers to extract search info from `DiscoverAppLocatorParams`.

Needed for https://github.com/elastic/kibana/issues/148775

## Refactoring changes
* Moved some code from `src/plugins/discover/public/utils/sorting` to
`common`, which was needed in the server-side context.
* Moved the definition of the `SavedSearch` interface from
`src/plugins/saved_search/public` to `common`

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Tim Sullivan 2023-02-16 11:33:51 -07:00 committed by GitHub
parent e7ebb0cf40
commit 807625c108
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1271 additions and 242 deletions

View file

@ -25,9 +25,15 @@ One folder for every "route", each folder contains files and folders related onl
Contains all the server-only code.
* **[/sample_data](./server/sample_data)** (Registrations with the Sample Data Registry for Discover saved objects)
* **[/capabilities_provider](./server/capabilities_provider.ts)** (CapabilitiesProvider definition of capabilities for Core)
* **[/ui_settings](./server/ui_settings.ts)** (Settings and the default values for UiSettingsServiceSetup )
* **[/locator](./server/locator)** (Extensions of DiscoverAppLocator for the DiscoverServerPlugin API)
### [src/plugins/discover/common](./common))
Contains all code shared by client and server.
* **[/constants](./common/constants.ts)** (General contants)
* **[/field_types](./common/field_types.ts)** (Field types constants)
* **[/locator](./common/locator)** (Registration with the URL service for BWC deep-linking to Discover views.)

View file

@ -0,0 +1,74 @@
/*
* 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 { getSort, getSortArray } from './get_sort';
import {
stubDataView,
stubDataViewWithoutTimeField,
} from '@kbn/data-views-plugin/common/data_view.stub';
describe('docTable', function () {
describe('getSort function', function () {
test('should be a function', function () {
expect(typeof getSort === 'function').toBeTruthy();
});
test('should return an array of objects', function () {
expect(getSort([['bytes', 'desc']], stubDataView)).toEqual([{ bytes: 'desc' }]);
expect(getSort([['bytes', 'desc']], stubDataViewWithoutTimeField)).toEqual([
{ bytes: 'desc' },
]);
});
test('should passthrough arrays of objects', () => {
expect(getSort([{ bytes: 'desc' }], stubDataView)).toEqual([{ bytes: 'desc' }]);
});
test('should return an empty array when passed an unsortable field', function () {
expect(getSort([['non-sortable', 'asc']], stubDataView)).toEqual([]);
expect(getSort([['lol_nope', 'asc']], stubDataView)).toEqual([]);
expect(getSort([['non-sortable', 'asc']], stubDataViewWithoutTimeField)).toEqual([]);
});
test('should return an empty array ', function () {
expect(getSort([], stubDataView)).toEqual([]);
expect(getSort([['foo', 'bar']], stubDataView)).toEqual([]);
expect(getSort([{ foo: 'bar' }], stubDataView)).toEqual([]);
});
test('should convert a legacy sort to an array of objects', function () {
expect(getSort(['foo', 'desc'], stubDataView)).toEqual([{ foo: 'desc' }]);
expect(getSort(['foo', 'asc'], stubDataView)).toEqual([{ foo: 'asc' }]);
});
});
describe('getSortArray function', function () {
test('should have an array method', function () {
expect(getSortArray).toBeInstanceOf(Function);
});
test('should return an array of arrays for sortable fields', function () {
expect(getSortArray([['bytes', 'desc']], stubDataView)).toEqual([['bytes', 'desc']]);
});
test('should return an array of arrays from an array of elasticsearch sort objects', function () {
expect(getSortArray([{ bytes: 'desc' }], stubDataView)).toEqual([['bytes', 'desc']]);
});
test('should sort by an empty array when an unsortable field is given', function () {
expect(getSortArray([{ 'non-sortable': 'asc' }], stubDataView)).toEqual([]);
expect(getSortArray([{ lol_nope: 'asc' }], stubDataView)).toEqual([]);
expect(getSortArray([{ 'non-sortable': 'asc' }], stubDataViewWithoutTimeField)).toEqual([]);
});
test('should return an empty array when passed an empty sort array', () => {
expect(getSortArray([], stubDataView)).toEqual([]);
expect(getSortArray([], stubDataViewWithoutTimeField)).toEqual([]);
});
});
});

View file

@ -0,0 +1,73 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { DataView } from '@kbn/data-views-plugin/common';
import type { SortOrder } from '@kbn/saved-search-plugin/public';
import { isPlainObject } from 'lodash';
export type SortPairObj = Record<string, string>;
export type SortPair = SortOrder | SortPairObj;
export type SortInput = SortPair | SortPair[];
export function isSortable(fieldName: string, dataView: DataView): boolean {
const field = dataView.getFieldByName(fieldName);
return !!(field && field.sortable);
}
function createSortObject(sortPair: SortInput, dataView: DataView): SortPairObj | undefined {
if (
Array.isArray(sortPair) &&
sortPair.length === 2 &&
isSortable(String(sortPair[0]), dataView)
) {
const [field, direction] = sortPair as SortOrder;
return { [field]: direction };
} else if (isPlainObject(sortPair) && isSortable(Object.keys(sortPair)[0], dataView)) {
return sortPair as SortPairObj;
}
}
export function isLegacySort(sort: SortPair[] | SortPair): sort is SortPair {
return (
sort.length === 2 && typeof sort[0] === 'string' && (sort[1] === 'desc' || sort[1] === 'asc')
);
}
/**
* Take a sorting array and make it into an object
* @param {array} sort two dimensional array [[fieldToSort, directionToSort]]
* or an array of objects [{fieldToSort: directionToSort}]
* @param {object} dataView used for determining default sort
* @returns Array<{object}> an array of sort objects
*/
export function getSort(sort: SortPair[] | SortPair, dataView: DataView): SortPairObj[] {
if (Array.isArray(sort)) {
if (isLegacySort(sort)) {
// To stay compatible with legacy sort, which just supported a single sort field
return [{ [sort[0]]: sort[1] }];
}
return sort
.map((sortPair: SortPair) => createSortObject(sortPair, dataView))
.filter((sortPairObj) => typeof sortPairObj === 'object') as SortPairObj[];
}
return [];
}
/**
* compared to getSort it doesn't return an array of objects, it returns an array of arrays
* [[fieldToSort: directionToSort]]
*/
export function getSortArray(sort: SortInput, dataView: DataView): SortOrder[] {
return getSort(sort, dataView).reduce((acc: SortOrder[], sortPair) => {
const entries = Object.entries(sortPair);
if (entries && entries[0]) {
acc.push(entries[0]);
}
return acc;
}, []);
}

View file

@ -0,0 +1,11 @@
/*
* 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 { getDefaultSort } from './get_default_sort';
export { getSort, getSortArray } from './get_sort';
export type { SortInput, SortPair } from './get_sort';
export { getSortForSearchSource } from './get_sort_for_search_source';

View file

@ -26,7 +26,4 @@ export {
getSavedSearchUrl,
getSavedSearchUrlConflictMessage,
throwErrorOnSavedSearchUrlConflict,
VIEW_MODE,
type DiscoverGridSettings,
type DiscoverGridSettingsColumn,
} from '@kbn/saved-search-plugin/public';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { getSort, getSortArray, getSortForEmbeddable } from './get_sort';
import { getSortForEmbeddable } from './get_sort';
import {
stubDataView,
stubDataViewWithoutTimeField,
@ -14,64 +14,6 @@ import {
import { uiSettingsMock } from '../../__mocks__/ui_settings';
describe('docTable', function () {
describe('getSort function', function () {
test('should be a function', function () {
expect(typeof getSort === 'function').toBeTruthy();
});
test('should return an array of objects', function () {
expect(getSort([['bytes', 'desc']], stubDataView)).toEqual([{ bytes: 'desc' }]);
expect(getSort([['bytes', 'desc']], stubDataViewWithoutTimeField)).toEqual([
{ bytes: 'desc' },
]);
});
test('should passthrough arrays of objects', () => {
expect(getSort([{ bytes: 'desc' }], stubDataView)).toEqual([{ bytes: 'desc' }]);
});
test('should return an empty array when passed an unsortable field', function () {
expect(getSort([['non-sortable', 'asc']], stubDataView)).toEqual([]);
expect(getSort([['lol_nope', 'asc']], stubDataView)).toEqual([]);
expect(getSort([['non-sortable', 'asc']], stubDataViewWithoutTimeField)).toEqual([]);
});
test('should return an empty array ', function () {
expect(getSort([], stubDataView)).toEqual([]);
expect(getSort([['foo', 'bar']], stubDataView)).toEqual([]);
expect(getSort([{ foo: 'bar' }], stubDataView)).toEqual([]);
});
test('should convert a legacy sort to an array of objects', function () {
expect(getSort(['foo', 'desc'], stubDataView)).toEqual([{ foo: 'desc' }]);
expect(getSort(['foo', 'asc'], stubDataView)).toEqual([{ foo: 'asc' }]);
});
});
describe('getSortArray function', function () {
test('should have an array method', function () {
expect(getSortArray).toBeInstanceOf(Function);
});
test('should return an array of arrays for sortable fields', function () {
expect(getSortArray([['bytes', 'desc']], stubDataView)).toEqual([['bytes', 'desc']]);
});
test('should return an array of arrays from an array of elasticsearch sort objects', function () {
expect(getSortArray([{ bytes: 'desc' }], stubDataView)).toEqual([['bytes', 'desc']]);
});
test('should sort by an empty array when an unsortable field is given', function () {
expect(getSortArray([{ 'non-sortable': 'asc' }], stubDataView)).toEqual([]);
expect(getSortArray([{ lol_nope: 'asc' }], stubDataView)).toEqual([]);
expect(getSortArray([{ 'non-sortable': 'asc' }], stubDataViewWithoutTimeField)).toEqual([]);
});
test('should return an empty array when passed an empty sort array', () => {
expect(getSortArray([], stubDataView)).toEqual([]);
expect(getSortArray([], stubDataViewWithoutTimeField)).toEqual([]);
});
});
describe('getSortForEmbeddable function', function () {
test('should return an array of arrays for sortable fields', function () {
expect(getSortForEmbeddable([['bytes', 'desc']], stubDataView)).toEqual([['bytes', 'desc']]);

View file

@ -6,74 +6,11 @@
* Side Public License, v 1.
*/
import { isPlainObject } from 'lodash';
import { DataView } from '@kbn/data-views-plugin/public';
import { DataView } from '@kbn/data-views-plugin/common';
import { IUiSettingsClient } from '@kbn/core/public';
import type { SortOrder } from '@kbn/saved-search-plugin/public';
import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common';
import { getDefaultSort } from './get_default_sort';
export type SortPairObj = Record<string, string>;
export type SortPair = SortOrder | SortPairObj;
export type SortInput = SortPair | SortPair[];
export function isSortable(fieldName: string, dataView: DataView): boolean {
const field = dataView.getFieldByName(fieldName);
return !!(field && field.sortable);
}
function createSortObject(sortPair: SortInput, dataView: DataView): SortPairObj | undefined {
if (
Array.isArray(sortPair) &&
sortPair.length === 2 &&
isSortable(String(sortPair[0]), dataView)
) {
const [field, direction] = sortPair as SortOrder;
return { [field]: direction };
} else if (isPlainObject(sortPair) && isSortable(Object.keys(sortPair)[0], dataView)) {
return sortPair as SortPairObj;
}
}
export function isLegacySort(sort: SortPair[] | SortPair): sort is SortPair {
return (
sort.length === 2 && typeof sort[0] === 'string' && (sort[1] === 'desc' || sort[1] === 'asc')
);
}
/**
* Take a sorting array and make it into an object
* @param {array} sort two dimensional array [[fieldToSort, directionToSort]]
* or an array of objects [{fieldToSort: directionToSort}]
* @param {object} dataView used for determining default sort
* @returns Array<{object}> an array of sort objects
*/
export function getSort(sort: SortPair[] | SortPair, dataView: DataView): SortPairObj[] {
if (Array.isArray(sort)) {
if (isLegacySort(sort)) {
// To stay compatible with legacy sort, which just supported a single sort field
return [{ [sort[0]]: sort[1] }];
}
return sort
.map((sortPair: SortPair) => createSortObject(sortPair, dataView))
.filter((sortPairObj) => typeof sortPairObj === 'object') as SortPairObj[];
}
return [];
}
/**
* compared to getSort it doesn't return an array of objects, it returns an array of arrays
* [[fieldToSort: directionToSort]]
*/
export function getSortArray(sort: SortInput, dataView: DataView): SortOrder[] {
return getSort(sort, dataView).reduce((acc: SortOrder[], sortPair) => {
const entries = Object.entries(sortPair);
if (entries && entries[0]) {
acc.push(entries[0]);
}
return acc;
}, []);
}
import { getDefaultSort, getSortArray, SortInput } from '../../../common/utils/sorting';
/**
* sorting for embeddable, like getSortArray,but returning a default in the case the given sort or dataView is not valid

View file

@ -5,7 +5,9 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { getSort, getSortArray, getSortForEmbeddable } from './get_sort';
export { getSortForSearchSource } from './get_sort_for_search_source';
export { getDefaultSort } from './get_default_sort';
export type { SortPair } from './get_sort';
export { getDefaultSort } from '../../../common/utils/sorting/get_default_sort';
export { getSort, getSortArray } from '../../../common/utils/sorting/get_sort';
export type { SortPair } from '../../../common/utils/sorting/get_sort';
export { getSortForSearchSource } from '../../../common/utils/sorting/get_sort_for_search_source';
export { getSortForEmbeddable } from './get_sort';

View file

@ -6,6 +6,27 @@
* Side Public License, v 1.
*/
import { KibanaRequest } from '@kbn/core/server';
import { DataPluginStart } from '@kbn/data-plugin/server/plugin';
import { ColumnsFromLocatorFn, SearchSourceFromLocatorFn, TitleFromLocatorFn } from './locator';
import { DiscoverServerPlugin } from './plugin';
export interface DiscoverServerPluginStartDeps {
data: DataPluginStart;
}
export interface LocatorServiceScopedClient {
columnsFromLocator: ColumnsFromLocatorFn;
searchSourceFromLocator: SearchSourceFromLocatorFn;
titleFromLocator: TitleFromLocatorFn;
}
export interface DiscoverServerPluginLocatorService {
asScopedClient: (req: KibanaRequest<unknown>) => Promise<LocatorServiceScopedClient>;
}
export interface DiscoverServerPluginStart {
locator: DiscoverServerPluginLocatorService;
}
export const plugin = () => new DiscoverServerPlugin();

View file

@ -0,0 +1,141 @@
/*
* 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 { IUiSettingsClient, SavedObject, SavedObjectsClientContract } from '@kbn/core/server';
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
import { ISearchStartSearchSource, SearchSource } from '@kbn/data-plugin/common';
import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
import { dataPluginMock } from '@kbn/data-plugin/server/mocks';
import { DataView } from '@kbn/data-views-plugin/common';
import { createStubDataView } from '@kbn/data-views-plugin/common/stubs';
import { SavedSearchAttributes } from '@kbn/saved-search-plugin/common';
import { LocatorServicesDeps as Services } from '.';
import { DiscoverAppLocatorParams, DOC_HIDE_TIME_COLUMN_SETTING } from '../../common';
import { columnsFromLocatorFactory } from './columns_from_locator';
const mockSavedSearchId = 'abc-test-123';
// object returned by savedObjectsClient.get in testing
const defaultSavedSearch: SavedObject<SavedSearchAttributes> = {
type: 'search',
id: mockSavedSearchId,
references: [
{ id: '90943e30-9a47-11e8-b64d-95841ca0b247', name: 'testIndexRefName', type: 'index-pattern' },
],
attributes: {
title: '[Logs] Visits',
description: '',
columns: ['response', 'url', 'clientip', 'machine.os', 'tags'],
sort: [['test', '134']] as unknown as [],
kibanaSavedObjectMeta: {
searchSourceJSON:
'{"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"testIndexRefName"}',
},
} as unknown as SavedSearchAttributes,
};
const coreStart = coreMock.createStart();
let uiSettingsClient: IUiSettingsClient;
let soClient: SavedObjectsClientContract;
let searchSourceStart: ISearchStartSearchSource;
let mockServices: Services;
let mockSavedSearch: SavedObject<SavedSearchAttributes>;
let mockDataView: DataView;
// mock search source belonging to the saved search
let mockSearchSource: SearchSource;
// mock params containing the discover app locator
let mockPayload: Array<{ params: DiscoverAppLocatorParams }>;
beforeAll(async () => {
const dataStartMock = dataPluginMock.createStartContract();
const request = httpServerMock.createKibanaRequest();
soClient = coreStart.savedObjects.getScopedClient(request);
uiSettingsClient = coreMock.createStart().uiSettings.asScopedToClient(soClient);
searchSourceStart = await dataStartMock.search.searchSource.asScoped(request);
mockServices = {
searchSourceStart,
savedObjects: soClient,
uiSettings: uiSettingsClient,
};
const soClientGet = soClient.get;
soClient.get = jest.fn().mockImplementation((type, id) => {
if (id === mockSavedSearchId) return mockSavedSearch;
return soClientGet(type, id);
});
});
beforeEach(() => {
mockPayload = [{ params: { savedSearchId: mockSavedSearchId } }];
mockSavedSearch = { ...defaultSavedSearch, attributes: { ...defaultSavedSearch.attributes } };
mockDataView = createStubDataView({
spec: {
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
name: 'testIndexRefName',
timeFieldName: 'timestamp',
},
});
mockSearchSource = createSearchSourceMock();
mockSearchSource.setField('index', mockDataView);
searchSourceStart.create = jest.fn().mockResolvedValue(mockSearchSource);
const uiSettingsGet = uiSettingsClient.get;
uiSettingsClient.get = jest.fn().mockImplementation((key: string) => {
if (key === DOC_HIDE_TIME_COLUMN_SETTING) {
return false; // this is the default for the real setting
}
return uiSettingsGet(key);
});
});
test('with search source using columns with default time field', async () => {
const provider = columnsFromLocatorFactory(mockServices);
const columns = await provider(mockPayload[0].params);
expect(columns).toEqual(['timestamp', 'response', 'url', 'clientip', 'machine.os', 'tags']);
});
test('with search source using columns without time field in the DataView', async () => {
mockDataView = createStubDataView({
spec: {
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
name: 'testIndexRefName',
timeFieldName: undefined,
},
});
mockSearchSource.setField('index', mockDataView);
const provider = columnsFromLocatorFactory(mockServices);
const columns = await provider(mockPayload[0].params);
expect(columns).toEqual(['response', 'url', 'clientip', 'machine.os', 'tags']);
});
test('with search source using columns when DOC_HIDE_TIME_COLUMN_SETTING is true', async () => {
const uiSettingsGet = uiSettingsClient.get;
uiSettingsClient.get = jest.fn().mockImplementation((key: string) => {
if (key === DOC_HIDE_TIME_COLUMN_SETTING) {
return true;
}
return uiSettingsGet(key);
});
const provider = columnsFromLocatorFactory(mockServices);
const columns = await provider(mockPayload[0].params);
expect(columns).toEqual(['response', 'url', 'clientip', 'machine.os', 'tags']);
});
test('with saved search containing ["_source"]', async () => {
mockSavedSearch.attributes.columns = ['_source'];
const provider = columnsFromLocatorFactory(mockServices);
const columns = await provider(mockPayload[0].params);
expect(columns).not.toBeDefined(); // must erase the field since it can not be used for search query
});

View file

@ -0,0 +1,101 @@
/*
* 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 { DataView } from '@kbn/data-views-plugin/common';
import { SavedSearch } from '@kbn/saved-search-plugin/common';
import { getSavedSearch } from '@kbn/saved-search-plugin/server';
import { LocatorServicesDeps } from '.';
import {
DiscoverAppLocatorParams,
DOC_HIDE_TIME_COLUMN_SETTING,
SEARCH_FIELDS_FROM_SOURCE,
} from '../../common';
function isStringArray(arr: unknown | string[]): arr is string[] {
return Array.isArray(arr) && arr.every((p) => typeof p === 'string');
}
/**
* @internal
*/
export const getColumns = async (
services: LocatorServicesDeps,
index: DataView,
savedSearch: SavedSearch
) => {
const [hideTimeColumn, useFieldsFromSource] = await Promise.all([
services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING),
services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE),
]);
// Add/adjust columns from the saved search attributes and UI Settings
let columns: string[] | undefined;
let columnsNext: string[] | undefined;
let timeFieldName: string | undefined;
// ignore '_source' column: it may be the only column when the user wishes to export all fields
const columnsTemp = savedSearch.columns?.filter((col) => col !== '_source');
if (typeof columnsTemp !== 'undefined' && columnsTemp.length > 0 && isStringArray(columnsTemp)) {
columns = columnsTemp;
// conditionally add the time field column:
if (index?.timeFieldName && !hideTimeColumn) {
timeFieldName = index.timeFieldName;
}
if (timeFieldName && !columnsTemp.includes(timeFieldName)) {
columns = [timeFieldName, ...columns];
}
/*
* For querying performance, the searchSource object must have fields set.
* Otherwise, the requests will ask for all fields, even if only a few are really needed.
* Discover does not set fields, since having all fields is needed for the UI.
*/
if (!useFieldsFromSource && columns.length) {
columnsNext = columns;
}
}
return {
timeFieldName,
columns: columnsNext,
};
};
/**
* @internal
*/
export function columnsFromLocatorFactory(services: LocatorServicesDeps) {
/**
* Allows consumers to retrieve a set of selected fields from a search in DiscoverAppLocatorParams
*
* @public
*/
const columnsFromLocator = async (
params: DiscoverAppLocatorParams
): Promise<string[] | undefined> => {
if (!params.savedSearchId) {
throw new Error(`Saved Search ID is required in DiscoverAppLocatorParams`);
}
const savedSearch = await getSavedSearch(params.savedSearchId, services);
const index = savedSearch.searchSource.getField('index');
if (!index) {
throw new Error(`Search Source is missing the "index" field`);
}
const { columns } = await getColumns(services, index, savedSearch);
return columns;
};
return columnsFromLocator;
}
export type ColumnsFromLocatorFn = ReturnType<typeof columnsFromLocatorFactory>;

View file

@ -0,0 +1,33 @@
/*
* 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 { CoreStart, IUiSettingsClient, SavedObjectsClientContract } from '@kbn/core/server';
import { ISearchStartSearchSource } from '@kbn/data-plugin/common';
import { DiscoverServerPluginLocatorService, DiscoverServerPluginStartDeps } from '..';
import { getScopedClient } from './service';
export type { ColumnsFromLocatorFn } from './columns_from_locator';
export type { SearchSourceFromLocatorFn } from './searchsource_from_locator';
export type { TitleFromLocatorFn } from './title_from_locator';
/**
* @internal
*/
export interface LocatorServicesDeps {
searchSourceStart: ISearchStartSearchSource;
savedObjects: SavedObjectsClientContract;
uiSettings: IUiSettingsClient;
}
/**
* @internal
*/
export const initializeLocatorServices = (
core: CoreStart,
deps: DiscoverServerPluginStartDeps
): DiscoverServerPluginLocatorService => getScopedClient(core, deps);

View file

@ -0,0 +1,41 @@
/*
* 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 { KibanaRequest } from '@kbn/core/server';
import { SearchSource } from '@kbn/data-plugin/common';
import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
import { DiscoverServerPluginLocatorService, LocatorServiceScopedClient } from '..';
import { DiscoverAppLocatorParams } from '../../common';
export const createLocatorServiceMock = (): DiscoverServerPluginLocatorService => {
const mockFields = ['@timestamp', 'mock-message'];
const columnsFromLocatorMock = jest
.fn<Promise<string[]>, [DiscoverAppLocatorParams]>()
.mockResolvedValue(mockFields);
const searchSourceFromLocatorMock = jest
.fn<Promise<SearchSource>, [DiscoverAppLocatorParams]>()
.mockResolvedValue(createSearchSourceMock({ fields: mockFields }));
const titleFromLocatorMock = jest
.fn<Promise<string>, [DiscoverAppLocatorParams]>()
.mockResolvedValue('mock search title');
return {
asScopedClient: jest
.fn<Promise<LocatorServiceScopedClient>, [req: KibanaRequest]>()
.mockImplementation(() => {
return Promise.resolve({
columnsFromLocator: columnsFromLocatorMock,
searchSourceFromLocator: searchSourceFromLocatorMock,
titleFromLocator: titleFromLocatorMock,
} as LocatorServiceScopedClient);
}),
};
};

View file

@ -0,0 +1,178 @@
/*
* 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 { IUiSettingsClient, SavedObject, SavedObjectsClientContract } from '@kbn/core/server';
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
import { ISearchStartSearchSource, SearchSource } from '@kbn/data-plugin/common';
import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
import { dataPluginMock } from '@kbn/data-plugin/server/mocks';
import { DataView } from '@kbn/data-views-plugin/common';
import { createStubDataView } from '@kbn/data-views-plugin/common/stubs';
import { SavedSearchAttributes } from '@kbn/saved-search-plugin/common';
import { LocatorServicesDeps as Services } from '.';
import { DiscoverAppLocatorParams, DOC_HIDE_TIME_COLUMN_SETTING } from '../../common';
import { searchSourceFromLocatorFactory } from './searchsource_from_locator';
const mockSavedSearchId = 'abc-test-123';
// object returned by savedObjectsClient.get in testing
const defaultSavedSearch: SavedObject<SavedSearchAttributes> = {
type: 'search',
id: mockSavedSearchId,
references: [
{ id: '90943e30-9a47-11e8-b64d-95841ca0b247', name: 'testIndexRefName', type: 'index-pattern' },
],
attributes: {
title: '[Logs] Visits',
description: '',
columns: ['response', 'url', 'clientip', 'machine.os', 'tags'],
sort: [['test', '134']] as unknown as [],
kibanaSavedObjectMeta: {
searchSourceJSON:
'{"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"testIndexRefName"}',
},
} as unknown as SavedSearchAttributes,
};
const coreStart = coreMock.createStart();
let uiSettingsClient: IUiSettingsClient;
let soClient: SavedObjectsClientContract;
let searchSourceStart: ISearchStartSearchSource;
let mockServices: Services;
let mockSavedSearch: SavedObject<SavedSearchAttributes>;
let mockDataView: DataView;
// mock search source belonging to the saved search
let mockSearchSource: SearchSource;
// mock params containing the discover app locator
let mockPayload: Array<{ params: DiscoverAppLocatorParams }>;
beforeAll(async () => {
const dataStartMock = dataPluginMock.createStartContract();
const request = httpServerMock.createKibanaRequest();
soClient = coreStart.savedObjects.getScopedClient(request);
uiSettingsClient = coreMock.createStart().uiSettings.asScopedToClient(soClient);
searchSourceStart = await dataStartMock.search.searchSource.asScoped(request);
mockServices = {
searchSourceStart,
savedObjects: soClient,
uiSettings: uiSettingsClient,
};
const soClientGet = soClient.get;
soClient.get = jest.fn().mockImplementation((type, id) => {
if (id === mockSavedSearchId) return mockSavedSearch;
return soClientGet(type, id);
});
});
beforeEach(() => {
mockPayload = [{ params: { savedSearchId: mockSavedSearchId } }];
mockSavedSearch = { ...defaultSavedSearch, attributes: { ...defaultSavedSearch.attributes } };
mockDataView = createStubDataView({
spec: {
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
name: 'testIndexRefName',
timeFieldName: 'timestamp',
},
});
mockSearchSource = createSearchSourceMock();
mockSearchSource.setField('index', mockDataView);
searchSourceStart.create = jest.fn().mockResolvedValue(mockSearchSource);
const uiSettingsGet = uiSettingsClient.get;
uiSettingsClient.get = jest.fn().mockImplementation((key: string) => {
if (key === DOC_HIDE_TIME_COLUMN_SETTING) {
return false; // this is the default for the real setting
}
return uiSettingsGet(key);
});
});
test('with saved search containing a filter', async () => {
const testFilter = {
meta: { index: 'logstash-*' },
query: { term: { host: 'elastic.co' } },
};
mockSearchSource.setField('filter', testFilter);
const provider = searchSourceFromLocatorFactory(mockServices);
const searchSource = await provider(mockPayload[0].params);
expect(searchSource.getSerializedFields().filter).toEqual([testFilter]);
});
test('with locator params containing a filter', async () => {
const testFilter = {
meta: { index: 'logstash-*' },
query: { term: { host: 'elastic.co' } },
};
mockPayload = [{ params: { savedSearchId: mockSavedSearchId, filters: [testFilter] } }];
const provider = searchSourceFromLocatorFactory(mockServices);
const searchSource = await provider(mockPayload[0].params);
expect(searchSource.getSerializedFields().filter).toEqual([testFilter]);
});
test('with saved search and locator params both containing a filter', async () => {
// search source belonging to the saved search
mockSearchSource.setField('filter', {
meta: { index: 'logstash-*' },
query: { term: { host: 'elastic.co' } },
});
// locator params
mockPayload = [
{
params: {
savedSearchId: mockSavedSearchId,
filters: [
{
meta: { index: 'logstash-*' },
query: { term: { os: 'Palm Pilot' } },
},
],
},
},
];
const provider = searchSourceFromLocatorFactory(mockServices);
const searchSource = await provider(mockPayload[0].params);
expect(searchSource.getSerializedFields().filter).toEqual([
{ meta: { index: 'logstash-*' }, query: { term: { host: 'elastic.co' } } },
{ meta: { index: 'logstash-*' }, query: { term: { os: 'Palm Pilot' } } },
]);
});
test('with locator params containing a timeRange', async () => {
const testTimeRange = { from: 'now-15m', to: 'now', mode: 'absolute' as const };
mockPayload = [{ params: { savedSearchId: mockSavedSearchId, timeRange: testTimeRange } }];
const provider = searchSourceFromLocatorFactory(mockServices);
const searchSource = await provider(mockPayload[0].params);
expect(searchSource.getSerializedFields().filter).toEqual([
{
meta: {
index: '90943e30-9a47-11e8-b64d-95841ca0b247',
},
query: {
range: { timestamp: { format: 'strict_date_optional_time', gte: 'now-15m', lte: 'now' } },
},
},
]);
});
test('with saved search containing ["_source"]', async () => {
mockSavedSearch.attributes.columns = ['_source'];
const provider = searchSourceFromLocatorFactory(mockServices);
const searchSource = await provider(mockPayload[0].params);
expect(searchSource.getSerializedFields().fields).toEqual(['*']);
});

View file

@ -0,0 +1,159 @@
/*
* 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 { SearchSource, TimeRange } from '@kbn/data-plugin/common';
import { DataView } from '@kbn/data-views-plugin/common';
import { AggregateQuery, Filter, Query } from '@kbn/es-query';
import { SavedSearch } from '@kbn/saved-search-plugin/common';
import { getSavedSearch } from '@kbn/saved-search-plugin/server';
import { LocatorServicesDeps } from '.';
import { DiscoverAppLocatorParams } from '../../common';
import { getSortForSearchSource } from '../../common/utils/sorting';
import { getColumns } from './columns_from_locator';
// Shortcut for return type of searchSource.getField('filter');
type FilterResponse = undefined | Filter | Filter[] | (() => Filter | Filter[] | undefined);
// flattens filter objects coming from different sources
function normalizeFilter(savedSearchFilterTmp?: FilterResponse) {
let savedSearchFilter: Filter[] | undefined;
if (savedSearchFilterTmp && Array.isArray(savedSearchFilterTmp)) {
// can not include functions: could be recursive
savedSearchFilter = [...savedSearchFilterTmp.filter((f) => typeof f !== 'function')];
} else if (savedSearchFilterTmp && typeof savedSearchFilterTmp !== 'function') {
savedSearchFilter = [savedSearchFilterTmp];
}
return savedSearchFilter;
}
/*
* Combine the time range filter from the job request body with any filters that have been saved into the saved search object
* NOTE: if the filters that were saved into the search are NOT an array, it may be a function, and can not be supported.
*/
const getFilters = (
timeFieldName: string | undefined,
index: DataView,
savedSearch: SavedSearch,
searchSource: SearchSource,
params: DiscoverAppLocatorParams
) => {
const filters: Filter[] = [];
// Set a time range filter from (1) DiscoverAppLocatorParams or (2) SavedSearch
if (timeFieldName) {
const timeRange = params.timeRange
? params.timeRange
: savedSearch.timeRange
? (savedSearch.timeRange as TimeRange)
: null;
if (timeRange) {
filters.push({
meta: { index: index.id },
query: {
range: {
[timeFieldName]: {
format: 'strict_date_optional_time',
gte: timeRange.from,
lte: timeRange.to,
},
},
},
});
}
}
const savedSearchFilter = normalizeFilter(searchSource.getField('filter'));
if (savedSearchFilter) {
filters.push(...savedSearchFilter);
}
const paramsFilter = normalizeFilter(params.filters);
if (paramsFilter) {
filters.push(...paramsFilter);
}
return filters;
};
/*
* Pick the query from the job request body vs any query that has been saved into the saved search object.
*/
const getQuery = (searchSource: SearchSource, params: DiscoverAppLocatorParams) => {
let query: Query | AggregateQuery | undefined;
const paramsQuery = params.query;
const savedSearchQuery = searchSource.getField('query');
if (paramsQuery) {
query = paramsQuery;
} else if (savedSearchQuery) {
// NOTE: cannot combine 2 queries (using AND): query can not be an array in SearchSourceFields
query = savedSearchQuery;
}
return query;
};
/**
* @internal
*/
export function searchSourceFromLocatorFactory(services: LocatorServicesDeps) {
/**
* Allows consumers to transform DiscoverAppLocatorParams into a SearchSource object for querying.
*
* @public
*/
const searchSourceFromLocator = async (
params: DiscoverAppLocatorParams
): Promise<SearchSource> => {
if (!params.savedSearchId) {
throw new Error(`Saved Search ID is required in DiscoverAppLocatorParams`);
}
const savedSearch = await getSavedSearch(params.savedSearchId, services);
const searchSource = savedSearch.searchSource.createCopy();
const index = searchSource.getField('index');
if (!index) {
throw new Error(`Search Source is missing the "index" field`);
}
const { columns, timeFieldName } = await getColumns(services, index, savedSearch);
// Inject columns
if (columns) {
searchSource.setField('fields', columns);
} else {
searchSource.setField('fields', ['*']);
}
// Inject updated filters
const filters = getFilters(timeFieldName, index, savedSearch, searchSource, params);
if (filters.length > 0) {
searchSource.removeField('filter');
searchSource.setField('filter', filters);
}
// Inject query
const query = getQuery(searchSource, params);
if (query) {
searchSource.removeField('query');
searchSource.setField('query', query);
}
// Inject sort
if (savedSearch.sort) {
const sort = getSortForSearchSource(savedSearch.sort as Array<[string, string]>, index);
searchSource.setField('sort', sort);
}
return searchSource;
};
return searchSourceFromLocator;
}
export type SearchSourceFromLocatorFn = ReturnType<typeof searchSourceFromLocatorFactory>;

View file

@ -0,0 +1,33 @@
/*
* 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 { CoreStart, KibanaRequest } from '@kbn/core/server';
import { DiscoverServerPluginLocatorService, DiscoverServerPluginStartDeps } from '..';
import { columnsFromLocatorFactory } from './columns_from_locator';
import { searchSourceFromLocatorFactory } from './searchsource_from_locator';
import { titleFromLocatorFactory } from './title_from_locator';
export const getScopedClient = (
core: CoreStart,
deps: DiscoverServerPluginStartDeps
): DiscoverServerPluginLocatorService => {
return {
asScopedClient: async (req: KibanaRequest<unknown>) => {
const searchSourceStart = await deps.data.search.searchSource.asScoped(req);
const savedObjects = core.savedObjects.getScopedClient(req);
const uiSettings = core.uiSettings.asScopedToClient(savedObjects);
const services = { searchSourceStart, savedObjects, uiSettings };
return {
columnsFromLocator: columnsFromLocatorFactory(services),
searchSourceFromLocator: searchSourceFromLocatorFactory(services),
titleFromLocator: titleFromLocatorFactory(services),
};
},
};
};

View file

@ -0,0 +1,110 @@
/*
* 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 { IUiSettingsClient, SavedObject, SavedObjectsClientContract } from '@kbn/core/server';
import { coreMock, httpServerMock } from '@kbn/core/server/mocks';
import { ISearchStartSearchSource } from '@kbn/data-plugin/common';
import { dataPluginMock } from '@kbn/data-plugin/server/mocks';
import { SavedSearchAttributes } from '@kbn/saved-search-plugin/common';
import { LocatorServicesDeps as Services } from '.';
import { DiscoverAppLocatorParams, DOC_HIDE_TIME_COLUMN_SETTING } from '../../common';
import { titleFromLocatorFactory } from './title_from_locator';
const mockSavedSearchId = 'abc-test-123';
const defaultSavedSearch: SavedObject<SavedSearchAttributes> = {
type: 'search',
id: mockSavedSearchId,
references: [
{ id: '90943e30-9a47-11e8-b64d-95841ca0b247', name: 'testIndexRefName', type: 'index-pattern' },
],
attributes: {
title: '[Logs] Visits',
description: '',
columns: ['response', 'url', 'clientip', 'machine.os', 'tags'],
sort: [['test', '134']] as unknown as [],
kibanaSavedObjectMeta: {
searchSourceJSON:
'{"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"testIndexRefName"}',
},
} as unknown as SavedSearchAttributes,
};
const coreStart = coreMock.createStart();
let uiSettingsClient: IUiSettingsClient;
let soClient: SavedObjectsClientContract;
let searchSourceStart: ISearchStartSearchSource;
let mockServices: Services;
let mockSavedSearch: SavedObject<SavedSearchAttributes>;
// mock params containing the discover app locator
let mockPayload: Array<{ params: DiscoverAppLocatorParams }>;
beforeAll(async () => {
const dataStartMock = dataPluginMock.createStartContract();
const request = httpServerMock.createKibanaRequest();
soClient = coreStart.savedObjects.getScopedClient(request);
uiSettingsClient = coreMock.createStart().uiSettings.asScopedToClient(soClient);
searchSourceStart = await dataStartMock.search.searchSource.asScoped(request);
mockServices = {
searchSourceStart,
savedObjects: soClient,
uiSettings: uiSettingsClient,
};
const soClientGet = soClient.get;
soClient.get = jest.fn().mockImplementation((type, id) => {
if (id === mockSavedSearchId) return mockSavedSearch;
return soClientGet(type, id);
});
});
beforeEach(() => {
mockPayload = [{ params: { savedSearchId: mockSavedSearchId } }];
mockSavedSearch = { ...defaultSavedSearch, attributes: { ...defaultSavedSearch.attributes } };
const uiSettingsGet = uiSettingsClient.get;
uiSettingsClient.get = jest.fn().mockImplementation((key: string) => {
if (key === DOC_HIDE_TIME_COLUMN_SETTING) {
return false; // this is the default for the real setting
}
return uiSettingsGet(key);
});
});
test(`retrieves title from DiscoverAppLocatorParams`, async () => {
const testTitle = 'Test Title from DiscoverAppLocatorParams';
mockPayload = [{ params: { title: testTitle } }];
const provider = titleFromLocatorFactory(mockServices);
const title = await provider(mockPayload[0].params);
expect(title).toBe(testTitle);
});
test(`retrieves title from saved search contents`, async () => {
const testTitle = 'Test Title from Saved Search Contents';
mockSavedSearch = {
...defaultSavedSearch,
attributes: { ...defaultSavedSearch.attributes, title: testTitle },
};
const provider = titleFromLocatorFactory(mockServices);
const title = await provider(mockPayload[0].params);
expect(title).toBe(testTitle);
});
test(`throws error if DiscoverAppLocatorParams do not contain a saved search ID`, async () => {
const testFn = async () => {
mockPayload = [{ params: { dataViewId: 'not-yet-supported' } }];
const provider = titleFromLocatorFactory(mockServices);
return await provider(mockPayload[0].params);
};
expect(testFn).rejects.toEqual(
new Error('DiscoverAppLocatorParams must contain a saved search reference')
);
});

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { SavedObject } from '@kbn/core/server';
import { LocatorServicesDeps } from '.';
import { DiscoverAppLocatorParams } from '../../common';
/**
* @internal
*/
export const titleFromLocatorFactory = (services: LocatorServicesDeps) => {
/**
* Allows consumers to derive a title of a search in Disocver from DiscoverAppLocatorParams.
* For now, this assumes the DiscoverAppLocatorParams contain a reference to a saved search. In the future,
* the params may only contain a reference to a DataView
*
* @public
*/
const titleFromLocator = async (params: DiscoverAppLocatorParams): Promise<string> => {
const { savedSearchId, title: paramsTitle } = params as {
savedSearchId?: string;
title?: string;
};
if (paramsTitle) {
return paramsTitle;
}
if (!savedSearchId) {
throw new Error(`DiscoverAppLocatorParams must contain a saved search reference`);
}
const { savedObjects } = services;
const searchObject: SavedObject<{ title?: string }> = await savedObjects.get(
'search',
savedSearchId // assumes params contains saved search reference
);
return (
searchObject.attributes.title ??
i18n.translate('discover.serverLocatorExtension.titleFromLocatorUnknown', {
defaultMessage: 'Unknown search',
})
);
};
return titleFromLocator;
};
export type TitleFromLocatorFn = ReturnType<typeof titleFromLocatorFactory>;

View file

@ -0,0 +1,16 @@
/*
* 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 { DiscoverServerPluginStart } from '.';
import { createLocatorServiceMock } from './locator/mocks';
export const discoverPluginMock = {
createStartContract: (): DiscoverServerPluginStart => ({
locator: createLocatorServiceMock(),
}),
};

View file

@ -6,17 +6,21 @@
* Side Public License, v 1.
*/
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
import type { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server';
import type { HomeServerPluginSetup } from '@kbn/home-plugin/server';
import { SharePluginSetup } from '@kbn/share-plugin/server';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common';
import { getUiSettings } from './ui_settings';
import { capabilitiesProvider } from './capabilities_provider';
import { registerSampleData } from './sample_data';
import type { SharePluginSetup } from '@kbn/share-plugin/server';
import type { DiscoverServerPluginStart, DiscoverServerPluginStartDeps } from '.';
import { DiscoverAppLocatorDefinition } from '../common/locator';
import { capabilitiesProvider } from './capabilities_provider';
import { initializeLocatorServices } from './locator';
import { registerSampleData } from './sample_data';
import { getUiSettings } from './ui_settings';
export class DiscoverServerPlugin implements Plugin<object, object> {
export class DiscoverServerPlugin
implements Plugin<object, DiscoverServerPluginStart, object, DiscoverServerPluginStartDeps>
{
public setup(
core: CoreSetup,
plugins: {
@ -41,8 +45,8 @@ export class DiscoverServerPlugin implements Plugin<object, object> {
return {};
}
public start(core: CoreStart) {
return {};
public start(core: CoreStart, deps: DiscoverServerPluginStartDeps) {
return { locator: initializeLocatorServices(core, deps) };
}
public stop() {}

View file

@ -7,3 +7,16 @@
*/
export { getSavedSearchUrl, getSavedSearchFullPathUrl } from './saved_searches_url';
export { fromSavedSearchAttributes } from './saved_searches_utils';
export type {
DiscoverGridSettings,
DiscoverGridSettingsColumn,
SavedSearch,
SavedSearchAttributes,
} from './types';
export enum VIEW_MODE {
DOCUMENT_LEVEL = 'documents',
AGGREGATED_LEVEL = 'aggregated',
}

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 { SavedSearch, SavedSearchAttributes } from '.';
export const fromSavedSearchAttributes = (
id: string,
attributes: SavedSearchAttributes,
tags: string[] | undefined,
searchSource: SavedSearch['searchSource']
): SavedSearch => ({
id,
searchSource,
title: attributes.title,
sort: attributes.sort,
columns: attributes.columns,
description: attributes.description,
tags,
grid: attributes.grid,
hideChart: attributes.hideChart,
viewMode: attributes.viewMode,
hideAggregatedPreview: attributes.hideAggregatedPreview,
rowHeight: attributes.rowHeight,
isTextBasedQuery: attributes.isTextBasedQuery,
usesAdHocDataView: attributes.usesAdHocDataView,
timeRestore: attributes.timeRestore,
timeRange: attributes.timeRange,
refreshInterval: attributes.refreshInterval,
rowsPerPage: attributes.rowsPerPage,
breakdownField: attributes.breakdownField,
});

View file

@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { ISearchSource, RefreshInterval, TimeRange } from '@kbn/data-plugin/common';
import { VIEW_MODE } from '.';
export interface DiscoverGridSettings {
columns?: Record<string, DiscoverGridSettingsColumn>;
}
export interface DiscoverGridSettingsColumn {
width?: number;
}
/** @internal **/
export interface SavedSearchAttributes {
title: string;
sort: Array<[string, string]>;
columns: string[];
description: string;
grid: {
columns?: Record<string, DiscoverGridSettingsColumn>;
};
hideChart: boolean;
isTextBasedQuery: boolean;
usesAdHocDataView?: boolean;
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
viewMode?: VIEW_MODE;
hideAggregatedPreview?: boolean;
rowHeight?: number;
timeRestore?: boolean;
timeRange?: TimeRange;
refreshInterval?: RefreshInterval;
rowsPerPage?: number;
breakdownField?: string;
}
/** @internal **/
export type SortOrder = [string, string];
/** @public **/
export interface SavedSearch {
searchSource: ISearchSource;
id?: string;
title?: string;
sort?: SortOrder[];
columns?: string[];
description?: string;
tags?: string[] | undefined;
grid?: {
columns?: Record<string, DiscoverGridSettingsColumn>;
};
hideChart?: boolean;
viewMode?: VIEW_MODE;
hideAggregatedPreview?: boolean;
rowHeight?: number;
isTextBasedQuery?: boolean;
usesAdHocDataView?: boolean;
// for restoring time range with a saved search
timeRestore?: boolean;
timeRange?: TimeRange;
refreshInterval?: RefreshInterval;
rowsPerPage?: number;
breakdownField?: string;
}

View file

@ -6,7 +6,8 @@
* Side Public License, v 1.
*/
export type { SavedSearch, SaveSavedSearchOptions, SortOrder } from './services/saved_searches';
export type { SortOrder } from '../common/types';
export type { SavedSearch, SaveSavedSearchOptions } from './services/saved_searches';
export {
getSavedSearch,
getSavedSearchFullPathUrl,
@ -15,11 +16,7 @@ export {
throwErrorOnSavedSearchUrlConflict,
saveSavedSearch,
} from './services/saved_searches';
export type {
DiscoverGridSettings,
DiscoverGridSettingsColumn,
} from './services/saved_searches/types';
export { VIEW_MODE } from './services/saved_searches/types';
export { VIEW_MODE } from '../common';
export function plugin() {
return {

View file

@ -12,8 +12,8 @@ import { injectSearchSourceReferences, parseSearchSourceJSON } from '@kbn/data-p
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public';
import type { SpacesApi } from '@kbn/spaces-plugin/public';
import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { SavedSearchAttributes, SavedSearch } from './types';
import type { SavedSearchAttributes } from '../../../common';
import type { SavedSearch } from './types';
import { SAVED_SEARCH_TYPE } from './constants';
import { fromSavedSearchAttributes } from './saved_searches_utils';

View file

@ -16,4 +16,4 @@ export {
export type { SaveSavedSearchOptions } from './save_saved_searches';
export { saveSavedSearch } from './save_saved_searches';
export { SAVED_SEARCH_TYPE } from './constants';
export type { SavedSearch, SortOrder } from './types';
export type { SavedSearch } from './types';

View file

@ -7,8 +7,8 @@
*/
import type { SavedObjectsClientContract, SavedObjectsStart } from '@kbn/core/public';
import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { SavedSearch, SavedSearchAttributes } from './types';
import type { SavedSearchAttributes } from '../../../common';
import type { SavedSearch } from './types';
import { SAVED_SEARCH_TYPE } from './constants';
import { toSavedSearchAttributes } from './saved_searches_utils';

View file

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

View file

@ -6,7 +6,9 @@
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import type { SavedSearchAttributes, SavedSearch } from './types';
import type { SavedSearchAttributes } from '../../../common';
import { fromSavedSearchAttributes as fromSavedSearchAttributesCommon } from '../../../common';
import type { SavedSearch } from './types';
export { getSavedSearchUrl, getSavedSearchFullPathUrl } from '../../../common';
@ -31,26 +33,8 @@ export const fromSavedSearchAttributes = (
searchSource: SavedSearch['searchSource'],
sharingSavedObjectProps: SavedSearch['sharingSavedObjectProps']
): SavedSearch => ({
id,
searchSource,
...fromSavedSearchAttributesCommon(id, attributes, tags, searchSource),
sharingSavedObjectProps,
title: attributes.title,
sort: attributes.sort,
columns: attributes.columns,
description: attributes.description,
tags,
grid: attributes.grid,
hideChart: attributes.hideChart,
viewMode: attributes.viewMode,
hideAggregatedPreview: attributes.hideAggregatedPreview,
rowHeight: attributes.rowHeight,
isTextBasedQuery: attributes.isTextBasedQuery,
usesAdHocDataView: attributes.usesAdHocDataView,
timeRestore: attributes.timeRestore,
timeRange: attributes.timeRange,
refreshInterval: attributes.refreshInterval,
rowsPerPage: attributes.rowsPerPage,
breakdownField: attributes.breakdownField,
});
export const toSavedSearchAttributes = (

View file

@ -7,81 +7,14 @@
*/
import type { ResolvedSimpleSavedObject } from '@kbn/core/public';
import type { ISearchSource, RefreshInterval, TimeRange } from '@kbn/data-plugin/common';
export enum VIEW_MODE {
DOCUMENT_LEVEL = 'documents',
AGGREGATED_LEVEL = 'aggregated',
}
export interface DiscoverGridSettings {
columns?: Record<string, DiscoverGridSettingsColumn>;
}
export interface DiscoverGridSettingsColumn {
width?: number;
}
/** @internal **/
export interface SavedSearchAttributes {
title: string;
sort: Array<[string, string]>;
columns: string[];
description: string;
grid: {
columns?: Record<string, DiscoverGridSettingsColumn>;
};
hideChart: boolean;
isTextBasedQuery: boolean;
usesAdHocDataView?: boolean;
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
viewMode?: VIEW_MODE;
hideAggregatedPreview?: boolean;
rowHeight?: number;
timeRestore?: boolean;
timeRange?: TimeRange;
refreshInterval?: RefreshInterval;
rowsPerPage?: number;
breakdownField?: string;
}
/** @internal **/
export type SortOrder = [string, string];
import { SavedSearch as SavedSearchCommon } from '../../../common';
/** @public **/
export interface SavedSearch {
searchSource: ISearchSource;
id?: string;
title?: string;
sort?: SortOrder[];
columns?: string[];
description?: string;
tags?: string[] | undefined;
grid?: {
columns?: Record<string, DiscoverGridSettingsColumn>;
};
hideChart?: boolean;
export interface SavedSearch extends SavedSearchCommon {
sharingSavedObjectProps?: {
outcome?: ResolvedSimpleSavedObject['outcome'];
aliasTargetId?: ResolvedSimpleSavedObject['alias_target_id'];
aliasPurpose?: ResolvedSimpleSavedObject['alias_purpose'];
errorJSON?: string;
};
viewMode?: VIEW_MODE;
hideAggregatedPreview?: boolean;
rowHeight?: number;
isTextBasedQuery?: boolean;
usesAdHocDataView?: boolean;
// for restoring time range with a saved search
timeRestore?: boolean;
timeRange?: TimeRange;
refreshInterval?: RefreshInterval;
rowsPerPage?: number;
breakdownField?: string;
}

View file

@ -8,4 +8,6 @@
import { SavedSearchServerPlugin } from './plugin';
export { getSavedSearch } from './services/saved_searches';
export const plugin = () => new SavedSearchServerPlugin();

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SavedObject, SavedObjectsClientContract } from '@kbn/core/server';
import {
injectReferences,
ISearchStartSearchSource,
parseSearchSourceJSON,
} from '@kbn/data-plugin/common';
import { fromSavedSearchAttributes, SavedSearchAttributes } from '../../../common';
interface GetSavedSearchDependencies {
savedObjects: SavedObjectsClientContract;
searchSourceStart: ISearchStartSearchSource;
}
export const getSavedSearch = async (savedSearchId: string, deps: GetSavedSearchDependencies) => {
const savedSearch: SavedObject<SavedSearchAttributes> = await deps.savedObjects.get(
'search',
savedSearchId
);
const parsedSearchSourceJSON = parseSearchSourceJSON(
savedSearch.attributes.kibanaSavedObjectMeta?.searchSourceJSON ?? '{}'
);
const searchSourceValues = injectReferences(
parsedSearchSourceJSON as Parameters<typeof injectReferences>[0],
savedSearch.references
);
return fromSavedSearchAttributes(
savedSearchId,
savedSearch.attributes,
undefined,
await deps.searchSourceStart.create(searchSourceValues)
);
};

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 { getSavedSearch } from './get_saved_searches';