Skip to content

Commit

Permalink
Create Extensions of DiscoverAppLocator for DiscoverServerPlugin (#15…
Browse files Browse the repository at this point in the history
…0631)

## Summary

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

Needed for #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
  • Loading branch information
tsullivan authored Feb 16, 2023
1 parent e7ebb0c commit 807625c
Show file tree
Hide file tree
Showing 37 changed files with 1,270 additions and 241 deletions.
10 changes: 8 additions & 2 deletions src/plugins/discover/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 74 additions & 0 deletions src/plugins/discover/common/utils/sorting/get_sort.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 73 additions & 0 deletions src/plugins/discover/common/utils/sorting/get_sort.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions src/plugins/discover/common/utils/sorting/index.ts
Original file line number Diff line number Diff line change
@@ -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';
3 changes: 0 additions & 3 deletions src/plugins/discover/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,4 @@ export {
getSavedSearchUrl,
getSavedSearchUrlConflictMessage,
throwErrorOnSavedSearchUrlConflict,
VIEW_MODE,
type DiscoverGridSettings,
type DiscoverGridSettingsColumn,
} from '@kbn/saved-search-plugin/public';
60 changes: 1 addition & 59 deletions src/plugins/discover/public/utils/sorting/get_sort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,72 +6,14 @@
* Side Public License, v 1.
*/

import { getSort, getSortArray, getSortForEmbeddable } from './get_sort';
import { getSortForEmbeddable } from './get_sort';
import {
stubDataView,
stubDataViewWithoutTimeField,
} from '@kbn/data-views-plugin/common/data_view.stub';
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']]);
Expand Down
67 changes: 2 additions & 65 deletions src/plugins/discover/public/utils/sorting/get_sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions src/plugins/discover/public/utils/sorting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
21 changes: 21 additions & 0 deletions src/plugins/discover/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Loading

0 comments on commit 807625c

Please sign in to comment.