mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
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:
parent
e7ebb0cf40
commit
807625c108
37 changed files with 1271 additions and 242 deletions
|
@ -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.)
|
||||
|
|
74
src/plugins/discover/common/utils/sorting/get_sort.test.ts
Normal file
74
src/plugins/discover/common/utils/sorting/get_sort.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
});
|
73
src/plugins/discover/common/utils/sorting/get_sort.ts
Normal file
73
src/plugins/discover/common/utils/sorting/get_sort.ts
Normal 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;
|
||||
}, []);
|
||||
}
|
11
src/plugins/discover/common/utils/sorting/index.ts
Normal file
11
src/plugins/discover/common/utils/sorting/index.ts
Normal 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';
|
|
@ -26,7 +26,4 @@ export {
|
|||
getSavedSearchUrl,
|
||||
getSavedSearchUrlConflictMessage,
|
||||
throwErrorOnSavedSearchUrlConflict,
|
||||
VIEW_MODE,
|
||||
type DiscoverGridSettings,
|
||||
type DiscoverGridSettingsColumn,
|
||||
} from '@kbn/saved-search-plugin/public';
|
||||
|
|
|
@ -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']]);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
|
|
141
src/plugins/discover/server/locator/columns_from_locator.test.ts
Normal file
141
src/plugins/discover/server/locator/columns_from_locator.test.ts
Normal 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
|
||||
});
|
101
src/plugins/discover/server/locator/columns_from_locator.ts
Normal file
101
src/plugins/discover/server/locator/columns_from_locator.ts
Normal 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>;
|
33
src/plugins/discover/server/locator/index.ts
Normal file
33
src/plugins/discover/server/locator/index.ts
Normal 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);
|
41
src/plugins/discover/server/locator/mocks.ts
Normal file
41
src/plugins/discover/server/locator/mocks.ts
Normal 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);
|
||||
}),
|
||||
};
|
||||
};
|
|
@ -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(['*']);
|
||||
});
|
159
src/plugins/discover/server/locator/searchsource_from_locator.ts
Normal file
159
src/plugins/discover/server/locator/searchsource_from_locator.ts
Normal 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>;
|
33
src/plugins/discover/server/locator/service.ts
Normal file
33
src/plugins/discover/server/locator/service.ts
Normal 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),
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
110
src/plugins/discover/server/locator/title_from_locator.test.ts
Normal file
110
src/plugins/discover/server/locator/title_from_locator.test.ts
Normal 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')
|
||||
);
|
||||
});
|
56
src/plugins/discover/server/locator/title_from_locator.ts
Normal file
56
src/plugins/discover/server/locator/title_from_locator.ts
Normal 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>;
|
16
src/plugins/discover/server/mocks.ts
Normal file
16
src/plugins/discover/server/mocks.ts
Normal 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(),
|
||||
}),
|
||||
};
|
|
@ -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() {}
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
36
src/plugins/saved_search/common/saved_searches_utils.ts
Normal file
36
src/plugins/saved_search/common/saved_searches_utils.ts
Normal 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,
|
||||
});
|
76
src/plugins/saved_search/common/types.ts
Normal file
76
src/plugins/saved_search/common/types.ts
Normal 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;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -8,4 +8,6 @@
|
|||
|
||||
import { SavedSearchServerPlugin } from './plugin';
|
||||
|
||||
export { getSavedSearch } from './services/saved_searches';
|
||||
|
||||
export const plugin = () => new SavedSearchServerPlugin();
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
};
|
|
@ -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';
|
Loading…
Add table
Add a link
Reference in a new issue