diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59e3bd9232d8b..9cba1ee909f6d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,7 @@ A high level overview of our contributing guidelines. - [Instrumenting with Elastic APM](#instrumenting-with-elastic-apm) - [Debugging Unit Tests](#debugging-unit-tests) - [Unit Testing Plugins](#unit-testing-plugins) + - [Automated Accessibility Testing](#automated-accessibility-testing) - [Cross-browser compatibility](#cross-browser-compatibility) - [Testing compatibility locally](#testing-compatibility-locally) - [Running Browser Automation Tests](#running-browser-automation-tests) @@ -553,6 +554,23 @@ yarn test:mocha yarn test:browser --dev # remove the --dev flag to run them once and close ``` +### Automated Accessibility Testing + +To run the tests locally: + +1. In one terminal window run `node scripts/functional_tests_server --config test/accessibility/config.ts` +2. In another terminal window run `node scripts/functional_test_runner.js --config test/accessibility/config.ts` + +To run the x-pack tests, swap the config file out for `x-pack/test/accessibility/config.ts`. + +After the server is up, you can go to this instance of Kibana at `localhost:5620`. + +The testing is done using [axe](https://github.com/dequelabs/axe-core). The same thing that runs in CI, +can be run locally using their browser plugins: + +- [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US) +- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/) + ### Cross-browser Compatibility #### Testing Compatibility Locally diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 461d51a3e76e3..48d4f929b6851 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -6,7 +6,7 @@ recommended for the development of all Kibana plugins. Besides the content in this style guide, the following style guides may also apply to all development within the Kibana project. Please make sure to also read them: -- [Accessibility style guide](style_guides/accessibility_guide.md) +- [Accessibility style guide](https://elastic.github.io/eui/#/guidelines/accessibility) - [SASS style guide](https://elastic.github.io/eui/#/guidelines/sass) ## General @@ -45,10 +45,7 @@ This part contains style guide rules around general (framework agnostic) HTML us Use camel case for the values of attributes such as `id` and `data-test-subj` selectors. ```html - ``` @@ -74,6 +71,59 @@ It's important that when you write CSS/SASS selectors using classes, IDs, and at capitalization in the CSS matches that used in the HTML. HTML and CSS follow different case sensitivity rules, and we can avoid subtle gotchas by ensuring we use the same capitalization in both of them. +### How to generate ids? + +When labeling elements (and for some other accessibility tasks) you will often need +ids. Ids must be unique within the page i.e. no duplicate ids in the rendered DOM +at any time. + +Since we have some components that are used multiple times on the page, you must +make sure every instance of that component has a unique `id`. To make the generation +of those `id`s easier, you can use the `htmlIdGenerator` service in the `@elastic/eui`. + +A React component could use it as follows: + +```jsx +import { htmlIdGenerator } from '@elastic/eui'; + +render() { + // Create a new generator that will create ids deterministic + const htmlId = htmlIdGenerator(); + return (
+ + +
); +} +``` + +Each id generator you create by calling `htmlIdGenerator()` will generate unique but +deterministic ids. As you can see in the above example, that single generator +created the same id in the label's `htmlFor` as well as the input's `id`. + +A single generator instance will create the same id when passed the same argument +to the function multiple times. But two different generators will produce two different +ids for the same argument to the function, as you can see in the following example: + +```js +const generatorOne = htmlIdGenerator(); +const generatorTwo = htmlIdGenerator(); + +// Those statements are always true: +// Same generator +generatorOne('foo') === generatorOne('foo'); +generatorOne('foo') !== generatorOne('bar'); + +// Different generator +generatorOne('foo') !== generatorTwo('foo'); +``` + +This allows multiple instances of a single React component to now have different ids. +If you include the above React component multiple times in the same page, +each component instance will have a unique id, because each render method will use a different +id generator. + +You can also use this service outside of React. + ## API endpoints The following style guide rules are targeting development of server side API endpoints. @@ -90,7 +140,8 @@ API routes must start with the `/api/` path segment, and should be followed by t Kibana uses `snake_case` for the entire API, just like Elasticsearch. All urls, paths, query string parameters, values, and bodies should be `snake_case` formatted. -*Right:* +_Right:_ + ``` POST /api/kibana/index_patterns { @@ -108,19 +159,19 @@ The following style guide rules apply for working with TypeScript/JavaScript fil ### TypeScript vs. JavaScript -Whenever possible, write code in TypeScript instead of JavaScript, especially if it's new code. +Whenever possible, write code in TypeScript instead of JavaScript, especially if it's new code. Check out [TYPESCRIPT.md](TYPESCRIPT.md) for help with this process. ### Prefer modern JavaScript/TypeScript syntax You should prefer modern language features in a lot of cases, e.g.: -* Prefer `class` over `prototype` inheritance -* Prefer arrow function over function expressions -* Prefer arrow function over storing `this` (no `const self = this;`) -* Prefer template strings over string concatenation -* Prefer the spread operator for copying arrays (`[...arr]`) over `arr.slice()` -* Use optional chaining (`?.`) and nullish Coalescing (`??`) over `lodash.get` (and similar utilities) +- Prefer `class` over `prototype` inheritance +- Prefer arrow function over function expressions +- Prefer arrow function over storing `this` (no `const self = this;`) +- Prefer template strings over string concatenation +- Prefer the spread operator for copying arrays (`[...arr]`) over `arr.slice()` +- Use optional chaining (`?.`) and nullish Coalescing (`??`) over `lodash.get` (and similar utilities) ### Avoid mutability and state @@ -131,7 +182,7 @@ Instead, create new variables, and shallow copies of objects and arrays: ```js // good function addBar(foos, foo) { - const newFoo = {...foo, name: 'bar'}; + const newFoo = { ...foo, name: 'bar' }; return [...foos, newFoo]; } @@ -250,8 +301,8 @@ const second = arr[1]; ### Magic numbers/strings -These are numbers (or other values) simply used in line in your code. *Do not -use these*, give them a variable name so they can be understood and changed +These are numbers (or other values) simply used in line in your code. _Do not +use these_, give them a variable name so they can be understood and changed easily. ```js @@ -325,19 +376,18 @@ import inSibling from '../foo/child'; Don't do this. Everything should be wrapped in a module that can be depended on by other modules. Even things as simple as a single value should be a module. - ### Only use ternary operators for small, simple code -And *never* use multiple ternaries together, because they make it more +And _never_ use multiple ternaries together, because they make it more difficult to reason about how different values flow through the conditions involved. Instead, structure the logic for maximum readability. ```js // good, a situation where only 1 ternary is needed -const foo = (a === b) ? 1 : 2; +const foo = a === b ? 1 : 2; // bad -const foo = (a === b) ? 1 : (a === c) ? 2 : 3; +const foo = a === b ? 1 : a === c ? 2 : 3; ``` ### Use descriptive conditions @@ -475,13 +525,12 @@ setTimeout(() => { Use slashes for both single line and multi line comments. Try to write comments that explain higher level mechanisms or clarify difficult -segments of your code. *Don't use comments to restate trivial things*. +segments of your code. _Don't use comments to restate trivial things_. -*Exception:* Comment blocks describing a function and its arguments +_Exception:_ Comment blocks describing a function and its arguments (docblock) should start with `/**`, contain a single `*` at the beginning of each line, and end with `*/`. - ```js // good @@ -546,11 +595,17 @@ You can read more about these two ngReact methods [here](https://github.com/ngRe Using `react-component` means adding a bunch of components into angular, while `reactDirective` keeps them isolated, and is also a more succinct syntax. **Good:** + ```html - + ``` **Bad:** + ```html ``` @@ -564,9 +619,9 @@ Name action functions in the form of a strong verb and passed properties in the ``` -## Attribution +## Attribution -Parts of the JavaScript style guide were initially forked from the -[node style guide](https://github.com/felixge/node-style-guide) created by [Felix Geisendörfer](http://felixge.de/) which is -licensed under the [CC BY-SA 3.0](http://creativecommons.org/licenses/by-sa/3.0/) +Parts of the JavaScript style guide were initially forked from the +[node style guide](https://github.com/felixge/node-style-guide) created by [Felix Geisendörfer](http://felixge.de/) which is +licensed under the [CC BY-SA 3.0](http://creativecommons.org/licenses/by-sa/3.0/) license. diff --git a/docs/api/spaces-management/get_all.asciidoc b/docs/api/spaces-management/get_all.asciidoc index 4ae67a0dcca8b..f7fb92baa165f 100644 --- a/docs/api/spaces-management/get_all.asciidoc +++ b/docs/api/spaces-management/get_all.asciidoc @@ -42,7 +42,7 @@ The API returns the following: "color": "#aabbcc", "disabledFeatures": ["apm"], "initials": "MK", - "imageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSU", + "imageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSU" }, { "id": "sales", @@ -50,6 +50,6 @@ The API returns the following: "initials": "MK", "disabledFeatures": ["discover", "timelion"], "imageUrl": "" - }, + } ] -------------------------------------------------- diff --git a/docs/development/core/public/kibana-plugin-public.simplesavedobject._constructor_.md b/docs/development/core/public/kibana-plugin-public.simplesavedobject._constructor_.md index f0769c0124d63..87d317da7a936 100644 --- a/docs/development/core/public/kibana-plugin-public.simplesavedobject._constructor_.md +++ b/docs/development/core/public/kibana-plugin-public.simplesavedobject._constructor_.md @@ -9,13 +9,13 @@ Constructs a new instance of the `SimpleSavedObject` class Signature: ```typescript -constructor(client: SavedObjectsClient, { id, type, version, attributes, error, references, migrationVersion }: SavedObjectType); +constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion }: SavedObjectType); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| client | SavedObjectsClient | | +| client | SavedObjectsClientContract | | | { id, type, version, attributes, error, references, migrationVersion } | SavedObjectType<T> | | diff --git a/src/core/TESTING.md b/src/core/TESTING.md index 4dfab8830a506..aac54a4a14680 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -2,15 +2,34 @@ This document outlines best practices and patterns for testing Kibana Plugins. -- [Strategy](#strategy) -- [Core Integrations](#core-integrations) - - [Core Mocks](#core-mocks) +- [Testing Kibana Plugins](#testing-kibana-plugins) + - [Strategy](#strategy) + - [New concerns in the Kibana Platform](#new-concerns-in-the-kibana-platform) + - [Core Integrations](#core-integrations) + - [Core Mocks](#core-mocks) + - [Example](#example) - [Strategies for specific Core APIs](#strategies-for-specific-core-apis) - - [HTTP Routes](#http-routes) - - [SavedObjects](#savedobjects) - - [Elasticsearch](#elasticsearch) -- [Plugin Integrations](#plugin-integrations) -- [Plugin Contracts](#plugin-contracts) + - [HTTP Routes](#http-routes) + - [Preconditions](#preconditions) + - [Unit testing](#unit-testing) + - [Example](#example-1) + - [Integration tests](#integration-tests) + - [Functional Test Runner](#functional-test-runner) + - [Example](#example-2) + - [TestUtils](#testutils) + - [Example](#example-3) + - [Applications](#applications) + - [Example](#example-4) + - [SavedObjects](#savedobjects) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests-1) + - [Elasticsearch](#elasticsearch) + - [Plugin integrations](#plugin-integrations) + - [Preconditions](#preconditions-1) + - [Testing dependencies usages](#testing-dependencies-usages) + - [Testing components consuming the dependencies](#testing-components-consuming-the-dependencies) + - [Testing optional plugin dependencies](#testing-optional-plugin-dependencies) + - [Plugin Contracts](#plugin-contracts) ## Strategy @@ -540,11 +559,232 @@ describe('renderApp', () => { }); ``` -#### SavedObjects +### SavedObjects -_How to test SO operations_ +#### Unit Tests -#### Elasticsearch +To unit test code that uses the Saved Objects client mock the client methods +and make assertions against the behaviour you would expect to see. + +Since the Saved Objects client makes network requests to an external +Elasticsearch cluster, it's important to include failure scenarios in your +test cases. + +When writing a view with which a user might interact, it's important to ensure +your code can recover from exceptions and provide a way for the user to +proceed. This behaviour should be tested as well. + +Below is an example of a Jest Unit test suite that mocks the server-side Saved +Objects client: + +```typescript +// src/plugins/myplugin/server/lib/short_url_lookup.ts +import crypto from 'crypto'; +import { SavedObjectsClientContract } from 'kibana/server'; + +export const shortUrlLookup = { + generateUrlId(url: string, savedObjectsClient: SavedObjectsClientContract) { + const id = crypto + .createHash('md5') + .update(url) + .digest('hex'); + + return savedObjectsClient + .create( + 'url', + { + url, + accessCount: 0, + createDate: new Date().valueOf(), + accessDate: new Date().valueOf(), + }, + { id } + ) + .then(doc => doc.id) + .catch(err => { + if (savedObjectsClient.errors.isConflictError(err)) { + return id; + } else { + throw err; + } + }); + }, +}; + +``` + +```typescript +// src/plugins/myplugin/server/lib/short_url_lookup.test.ts +import { shortUrlLookup } from './short_url_lookup'; +import { savedObjectsClientMock } from '../../../../../core/server/mocks'; + +describe('shortUrlLookup', () => { + const ID = 'bf00ad16941fc51420f91a93428b27a0'; + const TYPE = 'url'; + const URL = 'http://elastic.co'; + + const mockSavedObjectsClient = savedObjectsClientMock.create(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('generateUrlId', () => { + it('provides correct arguments to savedObjectsClient', async () => { + const ATTRIBUTES = { + url: URL, + accessCount: 0, + createDate: new Date().valueOf(), + accessDate: new Date().valueOf(), + }; + mockSavedObjectsClient.create.mockResolvedValueOnce({ + id: ID, + type: TYPE, + references: [], + attributes: ATTRIBUTES, + }); + await shortUrlLookup.generateUrlId(URL, mockSavedObjectsClient); + + expect(mockSavedObjectsClient.create).toHaveBeenCalledTimes(1); + const [type, attributes, options] = mockSavedObjectsClient.create.mock.calls[0]; + expect(type).toBe(TYPE); + expect(attributes).toStrictEqual(ATTRIBUTES); + expect(options).toStrictEqual({ id: ID }); + }); + + it('ignores version conflict and returns id', async () => { + mockSavedObjectsClient.create.mockRejectedValueOnce( + mockSavedObjectsClient.errors.decorateConflictError(new Error()) + ); + const id = await shortUrlLookup.generateUrlId(URL, mockSavedObjectsClient); + expect(id).toEqual(ID); + }); + + it('rejects with passed through savedObjectsClient errors', () => { + const error = new Error('oops'); + mockSavedObjectsClient.create.mockRejectedValueOnce(error); + return expect(shortUrlLookup.generateUrlId(URL, mockSavedObjectsClient)).rejects.toBe(error); + }); + }); +}); +``` + +The following is an example of a public saved object unit test. The biggest +difference with the server-side test is the slightly different Saved Objects +client API which returns `SimpleSavedObject` instances which needs to be +reflected in the mock. + +```typescript +// src/plugins/myplugin/public/saved_query_service.ts +import { + SavedObjectsClientContract, + SavedObjectAttributes, + SimpleSavedObject, +} from 'src/core/public'; + +export type SavedQueryAttributes = SavedObjectAttributes & { + title: string; + description: 'bar'; + query: { + language: 'kuery'; + query: 'response:200'; + }; +}; + +export const createSavedQueryService = (savedObjectsClient: SavedObjectsClientContract) => { + const saveQuery = async ( + attributes: SavedQueryAttributes + ): Promise> => { + try { + return await savedObjectsClient.create('query', attributes, { + id: attributes.title as string, + }); + } catch (err) { + throw new Error('Unable to create saved query, please try again.'); + } + }; + + return { + saveQuery, + }; +}; +``` + +```typescript +// src/plugins/myplugin/public/saved_query_service.test.ts +import { createSavedQueryService, SavedQueryAttributes } from './saved_query_service'; +import { savedObjectsServiceMock } from '../../../../../core/public/mocks'; +import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; + +describe('saved query service', () => { + const savedQueryAttributes: SavedQueryAttributes = { + title: 'foo', + description: 'bar', + query: { + language: 'kuery', + query: 'response:200', + }, + }; + + const mockSavedObjectsClient = savedObjectsServiceMock.createStartContract() + .client as jest.Mocked; + + const savedQueryService = createSavedQueryService(mockSavedObjectsClient); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('saveQuery', function() { + it('should create a saved object for the given attributes', async () => { + // The public Saved Objects client returns instances of + // SimpleSavedObject, so we create an instance to return from our mock. + const mockReturnValue = new SimpleSavedObject(mockSavedObjectsClient, { + type: 'query', + id: 'foo', + attributes: savedQueryAttributes, + references: [], + }); + mockSavedObjectsClient.create.mockResolvedValue(mockReturnValue); + + const response = await savedQueryService.saveQuery(savedQueryAttributes); + expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, { + id: 'foo', + }); + expect(response).toBe(mockReturnValue); + }); + + it('should reject with an error when saved objects client errors', async done => { + mockSavedObjectsClient.create.mockRejectedValue(new Error('timeout')); + + try { + await savedQueryService.saveQuery(savedQueryAttributes); + } catch (err) { + expect(err).toMatchInlineSnapshot( + `[Error: Unable to create saved query, please try again.]` + ); + done(); + } + }); + }); +}); +``` + +#### Integration Tests +To get the highest confidence in how your code behaves when using the Saved +Objects client, you should write at least a few integration tests which loads +data into and queries a real Elasticsearch database. + +To do that we'll write a Jest integration test using `TestUtils` to start +Kibana and esArchiver to load fixture data into Elasticsearch. + +1. Create the fixtures data you need in Elasticsearch +2. Create a fixtures archive with `node scripts/es_archiver save [index patterns...]` +3. Load the fixtures in your test using esArchiver `esArchiver.load('name')`; + +_todo: fully worked out example_ + +### Elasticsearch _How to test ES clients_ diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index d08c8b52e39c9..c3de645c6b17e 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -58,7 +58,7 @@ import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { LegacyPlatformService } from './legacy_service'; import { applicationServiceMock } from '../application/application_service.mock'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; -import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock'; +import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; const applicationSetup = applicationServiceMock.createInternalSetupContract(); @@ -97,7 +97,7 @@ const injectedMetadataStart = injectedMetadataServiceMock.createStartContract(); const notificationsStart = notificationServiceMock.createStartContract(); const overlayStart = overlayServiceMock.createStartContract(); const uiSettingsStart = uiSettingsServiceMock.createStartContract(); -const savedObjectsStart = savedObjectsMock.createStartContract(); +const savedObjectsStart = savedObjectsServiceMock.createStartContract(); const fatalErrorsStart = fatalErrorsServiceMock.createStartContract(); const mockStorage = { getItem: jest.fn() } as any; diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index ce90d49065ad4..3301d71e2cdaf 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -26,7 +26,7 @@ import { i18nServiceMock } from './i18n/i18n_service.mock'; import { notificationServiceMock } from './notifications/notifications_service.mock'; import { overlayServiceMock } from './overlays/overlay_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; -import { savedObjectsMock } from './saved_objects/saved_objects_service.mock'; +import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; import { contextServiceMock } from './context/context_service.mock'; import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; @@ -40,6 +40,7 @@ export { legacyPlatformServiceMock } from './legacy/legacy_service.mock'; export { notificationServiceMock } from './notifications/notifications_service.mock'; export { overlayServiceMock } from './overlays/overlay_service.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; +export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; function createCoreSetupMock({ basePath = '' } = {}) { const mock = { @@ -70,7 +71,7 @@ function createCoreStartMock({ basePath = '' } = {}) { notifications: notificationServiceMock.createStartContract(), overlays: overlayServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), - savedObjects: savedObjectsMock.createStartContract(), + savedObjects: savedObjectsServiceMock.createStartContract(), injectedMetadata: { getInjectedVar: injectedMetadataServiceMock.createStartContract().getInjectedVar, }, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index dbbcda8d60e12..688eaf4f2bfc7 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -44,7 +44,7 @@ import { injectedMetadataServiceMock } from '../injected_metadata/injected_metad import { httpServiceMock } from '../http/http_service.mock'; import { CoreSetup, CoreStart, PluginInitializerContext } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; -import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock'; +import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; export let mockPluginInitializers: Map; @@ -110,7 +110,7 @@ describe('PluginsService', () => { notifications: notificationServiceMock.createStartContract(), overlays: overlayServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), - savedObjects: savedObjectsMock.createStartContract(), + savedObjects: savedObjectsServiceMock.createStartContract(), fatalErrors: fatalErrorsServiceMock.createStartContract(), }; mockStartContext = { diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 91b4e9be58973..fb48524c20fb9 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1170,7 +1170,7 @@ export interface SavedObjectsUpdateOptions { // @public export class SimpleSavedObject { - constructor(client: SavedObjectsClient, { id, type, version, attributes, error, references, migrationVersion }: SavedObject); + constructor(client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion }: SavedObject); // (undocumented) attributes: T; // (undocumented) diff --git a/src/core/public/saved_objects/saved_objects_service.mock.ts b/src/core/public/saved_objects/saved_objects_service.mock.ts index 247e684a24b92..855bdf8314ec8 100644 --- a/src/core/public/saved_objects/saved_objects_service.mock.ts +++ b/src/core/public/saved_objects/saved_objects_service.mock.ts @@ -45,7 +45,7 @@ const createMock = () => { return mocked; }; -export const savedObjectsMock = { +export const savedObjectsServiceMock = { create: createMock, createStartContract: createStartContractMock, }; diff --git a/src/core/public/saved_objects/simple_saved_object.ts b/src/core/public/saved_objects/simple_saved_object.ts index 7978708c9eabb..8e464680bcf17 100644 --- a/src/core/public/saved_objects/simple_saved_object.ts +++ b/src/core/public/saved_objects/simple_saved_object.ts @@ -19,7 +19,7 @@ import { get, has, set } from 'lodash'; import { SavedObject as SavedObjectType, SavedObjectAttributes } from '../../server'; -import { SavedObjectsClient } from './saved_objects_client'; +import { SavedObjectsClientContract } from './saved_objects_client'; /** * This class is a very simple wrapper for SavedObjects loaded from the server @@ -41,7 +41,7 @@ export class SimpleSavedObject { public references: SavedObjectType['references']; constructor( - private client: SavedObjectsClient, + private client: SavedObjectsClientContract, { id, type, version, attributes, error, references, migrationVersion }: SavedObjectType ) { this.id = id; diff --git a/style_guides/accessibility_guide.md b/style_guides/accessibility_guide.md deleted file mode 100644 index 4827d93991510..0000000000000 --- a/style_guides/accessibility_guide.md +++ /dev/null @@ -1,73 +0,0 @@ -# Accessibility (A11Y) Guide - -[EUI's accessibility guidelines](https://elastic.github.io/eui/#/guidelines/accessibility) should be your first stop for all things. - -## Automated accessibility testing - -To run the tests locally: - -1. In one terminal window run `node scripts/functional_tests_server --config test/accessibility/config.ts` -2. In another terminal window run `node scripts/functional_test_runner.js --config test/accessibility/config.ts` - -To run the x-pack tests, swap the config file out for `x-pack/test/accessibility/config.ts`. - -After the server is up, you can go to this instance of Kibana at `localhost:5020`. - -The testing is done using [axe](https://github.com/dequelabs/axe-core). The same thing that runs in CI, -can be run locally using their browser plugins: - -- [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US) -- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/) - -## How to generate ids? - -When labeling elements (and for some other accessibility tasks) you will often need -ids. Ids must be unique within the page i.e. no duplicate ids in the rendered DOM -at any time. - -Since we have some components that are used multiple times on the page, you must -make sure every instance of that component has a unique `id`. To make the generation -of those `id`s easier, you can use the `htmlIdGenerator` service in the `@elastic/eui`. - -A react component could use it as follows: - -```jsx -import { htmlIdGenerator } from '@elastic/eui'; - -render() { - // Create a new generator that will create ids deterministic - const htmlId = htmlIdGenerator(); - return (
- - -
); -} -``` - -Each id generator you create by calling `htmlIdGenerator()` will generate unique but -deterministic ids. As you can see in the above example, that single generator -created the same id in the label's `htmlFor` as well as the input's `id`. - -A single generator instance will create the same id when passed the same argument -to the function multiple times. But two different generators will produce two different -ids for the same argument to the function, as you can see in the following example: - -```js -const generatorOne = htmlIdGenerator(); -const generatorTwo = htmlIdGenerator(); - -// Those statements are always true: -// Same generator -generatorOne('foo') === generatorOne('foo'); -generatorOne('foo') !== generatorOne('bar'); - -// Different generator -generatorOne('foo') !== generatorTwo('foo'); -``` - -This allows multiple instances of a single react component to now have different ids. -If you include the above react component multiple times in the same page, -each component instance will have a unique id, because each render method will use a different -id generator. - -You can use this service of course also outside of react. diff --git a/x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx b/x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx index dc0de6f6e9c69..5c793f670119c 100644 --- a/x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx +++ b/x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx @@ -8,29 +8,24 @@ import { EuiButtonEmpty, EuiInMemoryTable, EuiToolTip, EuiBasicTableColumn } fro import { i18n } from '@kbn/i18n'; import { last } from 'lodash'; -import React from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { createWaffleMapNode } from '../../containers/waffle/nodes_to_wafflemap'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; import { fieldToName } from '../waffle/lib/field_to_display_name'; import { NodeContextMenu } from '../waffle/node_context_menu'; import { InventoryItemType } from '../../../common/inventory_models/types'; import { SnapshotNode, SnapshotNodePath } from '../../../common/http_api/snapshot_api'; +import { ROOT_ELEMENT_ID } from '../../app'; interface Props { nodes: SnapshotNode[]; nodeType: InventoryItemType; options: InfraWaffleMapOptions; - formatter: (subject: string | number) => string; currentTime: number; + formatter: (subject: string | number) => string; onFilter: (filter: string) => void; } -const initialState = { - isPopoverOpen: [] as string[], -}; - -type State = Readonly; - const getGroupPaths = (path: SnapshotNodePath[]) => { switch (path.length) { case 3: @@ -42,126 +37,131 @@ const getGroupPaths = (path: SnapshotNodePath[]) => { } }; -export const TableView = class extends React.PureComponent { - public readonly state: State = initialState; - public render() { - const { nodes, options, formatter, currentTime, nodeType } = this.props; - const columns: Array> = [ - { - field: 'name', - name: i18n.translate('xpack.infra.tableView.columnName.name', { defaultMessage: 'Name' }), - sortable: true, - truncateText: true, - textOnly: true, - render: (value: string, item: { node: InfraWaffleMapNode }) => { - const tooltipText = item.node.id === value ? `${value}` : `${value} (${item.node.id})`; - // For the table we need to create a UniqueID that takes into to account the groupings - // as well as the node name. There is the possibility that a node can be present in two - // different groups and be on the screen at the same time. - const uniqueID = [...item.node.path.map(p => p.value), item.node.name].join(':'); - return ( - - - {value} - - - ); - }, - }, - ...options.groupBy.map((grouping, index) => ({ - field: `group_${index}`, - name: fieldToName((grouping && grouping.field) || ''), - sortable: true, - truncateText: true, - textOnly: true, - render: (value: string) => { - const handleClick = () => this.props.onFilter(`${grouping.field}:"${value}"`); - return ( - - {value} +export const TableView = (props: Props) => { + const { nodes, options, formatter, currentTime, nodeType } = props; + const [openPopovers, setOpenPopovers] = useState([]); + const openPopoverFor = useCallback( + (id: string) => () => { + setOpenPopovers([...openPopovers, id]); + }, + [openPopovers] + ); + + const closePopoverFor = useCallback( + (id: string) => () => { + if (openPopovers.includes(id)) { + setOpenPopovers(openPopovers.filter(subject => subject !== id)); + } + }, + [openPopovers] + ); + + useEffect(() => { + if (openPopovers.length > 0) { + document.getElementById(ROOT_ELEMENT_ID)!.style.overflowY = 'hidden'; + } else { + document.getElementById(ROOT_ELEMENT_ID)!.style.overflowY = 'auto'; + } + }, [openPopovers]); + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.infra.tableView.columnName.name', { defaultMessage: 'Name' }), + sortable: true, + truncateText: true, + textOnly: true, + render: (value: string, item: { node: InfraWaffleMapNode }) => { + const tooltipText = item.node.id === value ? `${value}` : `${value} (${item.node.id})`; + // For the table we need to create a UniqueID that takes into to account the groupings + // as well as the node name. There is the possibility that a node can be present in two + // different groups and be on the screen at the same time. + const uniqueID = [...item.node.path.map(p => p.value), item.node.name].join(':'); + return ( + + + {value} - ); - }, - })), - { - field: 'value', - name: i18n.translate('xpack.infra.tableView.columnName.last1m', { - defaultMessage: 'Last 1m', - }), - sortable: true, - truncateText: true, - dataType: 'number', - render: (value: number) => {formatter(value)}, - }, - { - field: 'avg', - name: i18n.translate('xpack.infra.tableView.columnName.avg', { defaultMessage: 'Avg' }), - sortable: true, - truncateText: true, - dataType: 'number', - render: (value: number) => {formatter(value)}, + + ); }, - { - field: 'max', - name: i18n.translate('xpack.infra.tableView.columnName.max', { defaultMessage: 'Max' }), - sortable: true, - truncateText: true, - dataType: 'number', - render: (value: number) => {formatter(value)}, + }, + ...options.groupBy.map((grouping, index) => ({ + field: `group_${index}`, + name: fieldToName((grouping && grouping.field) || ''), + sortable: true, + truncateText: true, + textOnly: true, + render: (value: string) => { + const handleClick = () => props.onFilter(`${grouping.field}:"${value}"`); + return ( + + {value} + + ); }, - ]; - const items = nodes.map(node => { - const name = last(node.path); - return { - name: (name && name.label) || 'unknown', - ...getGroupPaths(node.path).reduce( - (acc, path, index) => ({ - ...acc, - [`group_${index}`]: path.label, - }), - {} - ), - value: node.metric.value, - avg: node.metric.avg, - max: node.metric.max, - node: createWaffleMapNode(node), - }; - }); - const initialSorting = { - sort: { - field: 'value', - direction: 'desc', - }, - } as const; - return ( - - ); - } + })), + { + field: 'value', + name: i18n.translate('xpack.infra.tableView.columnName.last1m', { + defaultMessage: 'Last 1m', + }), + sortable: true, + truncateText: true, + dataType: 'number', + render: (value: number) => {formatter(value)}, + }, + { + field: 'avg', + name: i18n.translate('xpack.infra.tableView.columnName.avg', { defaultMessage: 'Avg' }), + sortable: true, + truncateText: true, + dataType: 'number', + render: (value: number) => {formatter(value)}, + }, + { + field: 'max', + name: i18n.translate('xpack.infra.tableView.columnName.max', { defaultMessage: 'Max' }), + sortable: true, + truncateText: true, + dataType: 'number', + render: (value: number) => {formatter(value)}, + }, + ]; - private openPopoverFor = (id: string) => () => { - this.setState(prevState => ({ isPopoverOpen: [...prevState.isPopoverOpen, id] })); - }; + const items = nodes.map(node => { + const name = last(node.path); + return { + name: (name && name.label) || 'unknown', + ...getGroupPaths(node.path).reduce( + (acc, path, index) => ({ + ...acc, + [`group_${index}`]: path.label, + }), + {} + ), + value: node.metric.value, + avg: node.metric.avg, + max: node.metric.max, + node: createWaffleMapNode(node), + }; + }); + const initialSorting = { + sort: { + field: 'value', + direction: 'desc', + }, + } as const; - private closePopoverFor = (id: string) => () => { - if (this.state.isPopoverOpen.includes(id)) { - this.setState(prevState => { - return { - isPopoverOpen: prevState.isPopoverOpen.filter(subject => subject !== id), - }; - }); - } - }; + return ( + + ); }; diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx index 01bff0b4f96e1..15d8b8b0e42b8 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/custom_field_panel.tsx @@ -8,10 +8,12 @@ import { EuiButton, EuiComboBox, EuiForm, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { IFieldType } from 'src/plugins/data/public'; +import { InfraGroupByOptions } from '../../lib/lib'; interface Props { onSubmit: (field: string) => void; fields: IFieldType[]; + currentOptions: InfraGroupByOptions[]; } interface SelectedOption { @@ -28,10 +30,16 @@ export const CustomFieldPanel = class extends React.PureComponent public static displayName = 'CustomFieldPanel'; public readonly state: State = initialState; public render() { - const { fields } = this.props; + const { fields, currentOptions } = this.props; const options = fields - .filter(f => f.aggregatable && f.type === 'string') + .filter( + f => + f.aggregatable && + f.type === 'string' && + !(currentOptions && currentOptions.some(o => o.field === f.name)) + ) .map(f => ({ label: f.name })); + const isSubmitDisabled = !this.state.selectedOptions.length; return (
@@ -57,7 +65,7 @@ export const CustomFieldPanel = class extends React.PureComponent /> = ({ options, currentTime, children, @@ -45,7 +44,7 @@ export const NodeContextMenu = ({ closePopover, nodeType, popoverPosition, -}: Props) => { +}) => { const uiCapabilities = useKibana().services.application?.capabilities; const inventoryModel = findInventoryModel(nodeType); const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; @@ -132,7 +131,7 @@ export const NodeContextMenu = ({ closePopover={closePopover} id={`${node.pathId}-popover`} isOpen={isPopoverOpen} - button={children} + button={children!} anchorPosition={popoverPosition} >
diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx index 003eeb96cc41c..3e697dccabac5 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx @@ -61,6 +61,10 @@ export const WaffleGroupByControls = class extends React.PureComponent= 2; + const maxGroupByTooltip = i18n.translate('xpack.infra.waffle.maxGroupByTooltip', { + defaultMessage: 'Only two groupings can be selected at a time', + }); const panels: EuiContextMenuPanelDescriptor[] = [ { id: 'firstPanel', @@ -72,6 +76,8 @@ export const WaffleGroupByControls = class extends React.PureComponent, + content: ( + + ), }, ]; const buttonBody = @@ -167,8 +183,8 @@ export const WaffleGroupByControls = class extends React.PureComponent { @@ -81,7 +81,7 @@ const mapToPositionUrlState = (value: any) => ? pickTimeKey(value) : undefined; -const mapToStreamLiveUrlState = (value: any) => (typeof value === 'boolean' ? value : undefined); +const mapToStreamLiveUrlState = (value: any) => (typeof value === 'boolean' ? value : false); export const replaceLogPositionInQueryString = (time: number) => Number.isNaN(time) @@ -91,4 +91,5 @@ export const replaceLogPositionInQueryString = (time: number) => time, tiebreaker: 0, }, + streamLive: false, }); diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx index a800b6421c027..a418be01d1ed2 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx @@ -19,7 +19,7 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); @@ -33,7 +33,7 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx index 5fa80c8efee73..2d1f3a32988aa 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx @@ -73,7 +73,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); @@ -89,7 +89,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); }); diff --git a/x-pack/legacy/plugins/ml/server/new_platform/data_analytics_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/data_analytics_schema.ts index f5d72c51dc070..21454fa884b82 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/data_analytics_schema.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/data_analytics_schema.ts @@ -13,8 +13,16 @@ export const dataAnalyticsJobConfigSchema = { results_field: schema.maybe(schema.string()), }), source: schema.object({ - index: schema.string(), + index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + query: schema.maybe(schema.any()), + _source: schema.maybe( + schema.object({ + includes: schema.maybe(schema.arrayOf(schema.maybe(schema.string()))), + excludes: schema.maybe(schema.arrayOf(schema.maybe(schema.string()))), + }) + ), }), + allow_lazy_start: schema.maybe(schema.boolean()), analysis: schema.any(), analyzed_fields: schema.any(), model_memory_limit: schema.string(), diff --git a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts index 67fa2fba46f1a..f134820adbb48 100644 --- a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts @@ -156,7 +156,7 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti params: schema.object({ analyticsId: schema.string(), }), - body: schema.object({ ...dataAnalyticsJobConfigSchema }), + body: schema.object(dataAnalyticsJobConfigSchema), }, }, licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { diff --git a/x-pack/legacy/plugins/monitoring/common/constants.ts b/x-pack/legacy/plugins/monitoring/common/constants.ts index 53764f592dc15..1fc322a0de395 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.ts +++ b/x-pack/legacy/plugins/monitoring/common/constants.ts @@ -159,8 +159,6 @@ export const INDEX_ALERTS = '.monitoring-alerts-6,.monitoring-alerts-7' + INDEX_ export const INDEX_PATTERN_ELASTICSEARCH = '.monitoring-es-6-*,.monitoring-es-7-*' + INDEX_PATTERN_ELASTICSEARCH_NEW; -export const INDEX_PATTERN_FILEBEAT = 'filebeat-*'; - // This is the unique token that exists in monitoring indices collected by metricbeat export const METRICBEAT_INDEX_NAME_UNIQUE_TOKEN = '-mb-'; diff --git a/x-pack/legacy/plugins/monitoring/config.js b/x-pack/legacy/plugins/monitoring/config.js index 778b656c056f2..ec4c00ccbea11 100644 --- a/x-pack/legacy/plugins/monitoring/config.js +++ b/x-pack/legacy/plugins/monitoring/config.js @@ -18,6 +18,9 @@ export const config = Joi => { enabled: Joi.boolean().default(true), ui: Joi.object({ enabled: Joi.boolean().default(true), + logs: Joi.object({ + index: Joi.string().default('filebeat-*'), + }).default(), ccs: Joi.object({ enabled: Joi.boolean().default(true), }).default(), diff --git a/x-pack/legacy/plugins/monitoring/index.js b/x-pack/legacy/plugins/monitoring/index.js index ade172f527dab..25b88958c116f 100644 --- a/x-pack/legacy/plugins/monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/index.js @@ -50,6 +50,7 @@ export const monitoring = kibana => 'monitoring.cluster_alerts.email_notifications.email_address', 'monitoring.ui.ccs.enabled', 'monitoring.ui.elasticsearch.logFetchCount', + 'monitoring.ui.logs.index', ]; const serverConfig = server.config(); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/logs/init_infra_source.js b/x-pack/legacy/plugins/monitoring/server/lib/logs/init_infra_source.js index f2b179fe014b3..7ca36e8b29553 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/logs/init_infra_source.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/logs/init_infra_source.js @@ -5,11 +5,15 @@ */ import { prefixIndexPattern } from '../ccs_utils'; -import { INDEX_PATTERN_FILEBEAT, INFRA_SOURCE_ID } from '../../../common/constants'; +import { INFRA_SOURCE_ID } from '../../../common/constants'; export const initInfraSource = (config, infraPlugin) => { if (infraPlugin) { - const filebeatIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_FILEBEAT, '*'); + const filebeatIndexPattern = prefixIndexPattern( + config, + config.get('monitoring.ui.logs.index'), + '*' + ); infraPlugin.defineInternalSourceConfiguration(INFRA_SOURCE_ID, { name: 'Elastic Stack Logs', logAlias: filebeatIndexPattern, diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/cluster/cluster.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/cluster/cluster.js index 5c85a10edbc29..b2c2db7ea793e 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/cluster/cluster.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/cluster/cluster.js @@ -9,7 +9,6 @@ import { getClustersFromRequest } from '../../../../lib/cluster/get_clusters_fro import { handleError } from '../../../../lib/errors'; import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; import { verifyCcsAvailability } from '../../../../lib/elasticsearch/verify_ccs_availability'; -import { INDEX_PATTERN_FILEBEAT } from '../../../../../common/constants'; export function clusterRoute(server) { /* @@ -37,9 +36,10 @@ export function clusterRoute(server) { }, handler: async req => { await verifyCcsAvailability(req); + const config = server.config(); const indexPatterns = getIndexPatterns(server, { - filebeatIndexPattern: INDEX_PATTERN_FILEBEAT, + filebeatIndexPattern: config.get('monitoring.ui.logs.index'), }); const options = { clusterUuid: req.params.clusterUuid, diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/cluster/clusters.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/cluster/clusters.js index 6342a9436f6c7..92f0367097228 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/cluster/clusters.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/cluster/clusters.js @@ -9,7 +9,6 @@ import { getClustersFromRequest } from '../../../../lib/cluster/get_clusters_fro import { verifyMonitoringAuth } from '../../../../lib/elasticsearch/verify_monitoring_auth'; import { verifyCcsAvailability } from '../../../../lib/elasticsearch/verify_ccs_availability'; import { handleError } from '../../../../lib/errors'; -import { INDEX_PATTERN_FILEBEAT } from '../../../../../common/constants'; import { getIndexPatterns } from '../../../../lib/cluster/get_index_patterns'; export function clustersRoute(server) { @@ -34,6 +33,7 @@ export function clustersRoute(server) { }, }, handler: async req => { + const config = server.config(); let clusters = []; // NOTE using try/catch because checkMonitoringAuth is expected to throw @@ -43,7 +43,7 @@ export function clustersRoute(server) { await verifyMonitoringAuth(req); await verifyCcsAvailability(req); const indexPatterns = getIndexPatterns(server, { - filebeatIndexPattern: INDEX_PATTERN_FILEBEAT, + filebeatIndexPattern: config.get('monitoring.ui.logs.index'), }); clusters = await getClustersFromRequest(req, indexPatterns, { codePaths: req.payload.codePaths, diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js index c32e25d9f20d1..6f03459acf285 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/elasticsearch/index_detail.js @@ -13,10 +13,7 @@ import { getShardAllocation, getShardStats } from '../../../../lib/elasticsearch import { handleError } from '../../../../lib/errors/handle_error'; import { prefixIndexPattern } from '../../../../lib/ccs_utils'; import { metricSet } from './metric_set_index_detail'; -import { - INDEX_PATTERN_ELASTICSEARCH, - INDEX_PATTERN_FILEBEAT, -} from '../../../../../common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs/get_logs'; const { advanced: metricSetAdvanced, overview: metricSetOverview } = metricSet; @@ -50,7 +47,11 @@ export function esIndexRoute(server) { const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); - const filebeatIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_FILEBEAT, ccs); + const filebeatIndexPattern = prefixIndexPattern( + config, + config.get('monitoring.ui.logs.index'), + ccs + ); const isAdvanced = req.payload.is_advanced; const metricSet = isAdvanced ? metricSetAdvanced : metricSetOverview; diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js index 25ead723e3ddb..364214d45c2da 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/elasticsearch/node_detail.js @@ -13,10 +13,7 @@ import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors/handle_error'; import { prefixIndexPattern } from '../../../../lib/ccs_utils'; import { metricSets } from './metric_set_node_detail'; -import { - INDEX_PATTERN_ELASTICSEARCH, - INDEX_PATTERN_FILEBEAT, -} from '../../../../../common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs/get_logs'; const { advanced: metricSetAdvanced, overview: metricSetOverview } = metricSets; @@ -51,7 +48,11 @@ export function esNodeRoute(server) { const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); - const filebeatIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_FILEBEAT, '*'); + const filebeatIndexPattern = prefixIndexPattern( + config, + config.get('monitoring.ui.logs.index'), + '*' + ); const isAdvanced = req.payload.is_advanced; let metricSet; diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js index b0045502fa228..df1e847c16606 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/elasticsearch/overview.js @@ -12,10 +12,7 @@ import { getMetrics } from '../../../../lib/details/get_metrics'; import { handleError } from '../../../../lib/errors/handle_error'; import { prefixIndexPattern } from '../../../../lib/ccs_utils'; import { metricSet } from './metric_set_overview'; -import { - INDEX_PATTERN_ELASTICSEARCH, - INDEX_PATTERN_FILEBEAT, -} from '../../../../../common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../../common/constants'; import { getLogs } from '../../../../lib/logs'; import { getIndicesUnassignedShardStats } from '../../../../lib/elasticsearch/shards/get_indices_unassigned_shard_stats'; @@ -42,7 +39,11 @@ export function esOverviewRoute(server) { const ccs = req.payload.ccs; const clusterUuid = req.params.clusterUuid; const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); - const filebeatIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_FILEBEAT, '*'); + const filebeatIndexPattern = prefixIndexPattern( + config, + config.get('monitoring.ui.logs.index'), + '*' + ); const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; diff --git a/x-pack/legacy/plugins/rollup/common/index.ts b/x-pack/legacy/plugins/rollup/common/index.ts index 800da79552a57..4229803462203 100644 --- a/x-pack/legacy/plugins/rollup/common/index.ts +++ b/x-pack/legacy/plugins/rollup/common/index.ts @@ -4,12 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LICENSE_TYPE_BASIC, LicenseType } from '../../../common/constants'; + export const PLUGIN = { ID: 'rollup', + MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, + getI18nName: (i18n: any): string => { + return i18n.translate('xpack.rollupJobs.appName', { + defaultMessage: 'Rollup jobs', + }); + }, }; export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns'; +export const API_BASE_PATH = '/api/rollup'; + export { UIM_APP_NAME, UIM_APP_LOAD, diff --git a/x-pack/legacy/plugins/rollup/index.js b/x-pack/legacy/plugins/rollup/index.ts similarity index 57% rename from x-pack/legacy/plugins/rollup/index.js rename to x-pack/legacy/plugins/rollup/index.ts index cace3bba1592b..7548af23b3aae 100644 --- a/x-pack/legacy/plugins/rollup/index.js +++ b/x-pack/legacy/plugins/rollup/index.ts @@ -5,20 +5,13 @@ */ import { resolve } from 'path'; -import { PLUGIN, CONFIG_ROLLUPS } from './common'; -import { registerLicenseChecker } from './server/lib/register_license_checker'; -import { rollupDataEnricher } from './rollup_data_enricher'; -import { registerRollupSearchStrategy } from './server/lib/search_strategies'; -import { - registerIndicesRoute, - registerFieldsForWildcardRoute, - registerSearchRoute, - registerJobsRoute, -} from './server/routes/api'; -import { registerRollupUsageCollector } from './server/usage'; import { i18n } from '@kbn/i18n'; +import { PluginInitializerContext } from 'src/core/server'; +import { RollupSetup } from '../../../plugins/rollup/server'; +import { PLUGIN, CONFIG_ROLLUPS } from './common'; +import { plugin } from './server'; -export function rollup(kibana) { +export function rollup(kibana: any) { return new kibana.Plugin({ id: PLUGIN.ID, configPrefix: 'xpack.rollup', @@ -45,22 +38,30 @@ export function rollup(kibana) { visualize: ['plugins/rollup/legacy'], search: ['plugins/rollup/legacy'], }, - init: function(server) { - const { usageCollection } = server.newPlatform.setup.plugins; - registerLicenseChecker(server); - registerIndicesRoute(server); - registerFieldsForWildcardRoute(server); - registerSearchRoute(server); - registerJobsRoute(server); - registerRollupUsageCollector(usageCollection, server); - if ( - server.plugins.index_management && - server.plugins.index_management.addIndexManagementDataEnricher - ) { - server.plugins.index_management.addIndexManagementDataEnricher(rollupDataEnricher); - } + init(server: any) { + const { core: coreSetup, plugins } = server.newPlatform.setup; + const { usageCollection, metrics } = plugins; + + const rollupSetup = (plugins.rollup as unknown) as RollupSetup; - registerRollupSearchStrategy(this.kbnServer); + const initContext = ({ + config: rollupSetup.__legacy.config, + logger: rollupSetup.__legacy.logger, + } as unknown) as PluginInitializerContext; + + const rollupPluginInstance = plugin(initContext); + + rollupPluginInstance.setup(coreSetup, { + usageCollection, + metrics, + __LEGACY: { + plugins: { + xpack_main: server.plugins.xpack_main, + rollup: server.plugins[PLUGIN.ID], + index_management: server.plugins.index_management, + }, + }, + }); }, }); } diff --git a/x-pack/legacy/plugins/rollup/kibana.json b/x-pack/legacy/plugins/rollup/kibana.json new file mode 100644 index 0000000000000..3781d59d8c0f3 --- /dev/null +++ b/x-pack/legacy/plugins/rollup/kibana.json @@ -0,0 +1,14 @@ +{ + "id": "rollup", + "version": "kibana", + "requiredPlugins": [ + "home", + "index_management", + "metrics" + ], + "optionalPlugins": [ + "usageCollection" + ], + "server": true, + "ui": false +} diff --git a/x-pack/legacy/plugins/rollup/server/client/elasticsearch_rollup.js b/x-pack/legacy/plugins/rollup/server/client/elasticsearch_rollup.ts similarity index 96% rename from x-pack/legacy/plugins/rollup/server/client/elasticsearch_rollup.js rename to x-pack/legacy/plugins/rollup/server/client/elasticsearch_rollup.ts index 3b073cd2139c1..840f66a056d2d 100644 --- a/x-pack/legacy/plugins/rollup/server/client/elasticsearch_rollup.js +++ b/x-pack/legacy/plugins/rollup/server/client/elasticsearch_rollup.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const elasticsearchJsPlugin = (Client, config, components) => { +export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { const ca = components.clientAction.factory; Client.prototype.rollup = components.clientAction.namespaceFactory(); diff --git a/x-pack/legacy/plugins/rollup/server/lib/is_es_error_factory/index.js b/x-pack/legacy/plugins/rollup/server/collectors/index.ts similarity index 80% rename from x-pack/legacy/plugins/rollup/server/lib/is_es_error_factory/index.js rename to x-pack/legacy/plugins/rollup/server/collectors/index.ts index 441648a8701e0..47c1bcb6c7248 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/is_es_error_factory/index.js +++ b/x-pack/legacy/plugins/rollup/server/collectors/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { isEsErrorFactory } from './is_es_error_factory'; +export { registerRollupUsageCollector } from './register'; diff --git a/x-pack/legacy/plugins/rollup/server/usage/collector.js b/x-pack/legacy/plugins/rollup/server/collectors/register.ts similarity index 83% rename from x-pack/legacy/plugins/rollup/server/usage/collector.js rename to x-pack/legacy/plugins/rollup/server/collectors/register.ts index 21c4de62c8fdc..02ad5dc92fd13 100644 --- a/x-pack/legacy/plugins/rollup/server/usage/collector.js +++ b/x-pack/legacy/plugins/rollup/server/collectors/register.ts @@ -5,25 +5,31 @@ */ import { get } from 'lodash'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + +interface IdToFlagMap { + [key: string]: boolean; +} const ROLLUP_USAGE_TYPE = 'rollups'; // elasticsearch index.max_result_window default value const ES_MAX_RESULT_WINDOW_DEFAULT_VALUE = 1000; -function getIdFromSavedObjectId(savedObjectId) { +function getIdFromSavedObjectId(savedObjectId: string) { // The saved object ID is formatted `{TYPE}:{ID}`. return savedObjectId.split(':')[1]; } -function createIdToFlagMap(ids) { +function createIdToFlagMap(ids: string[]) { return ids.reduce((map, id) => { map[id] = true; return map; - }, {}); + }, {} as any); } -async function fetchRollupIndexPatterns(kibanaIndex, callCluster) { +async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: CallCluster) { const searchParams = { size: ES_MAX_RESULT_WINDOW_DEFAULT_VALUE, index: kibanaIndex, @@ -50,7 +56,11 @@ async function fetchRollupIndexPatterns(kibanaIndex, callCluster) { }); } -async function fetchRollupSavedSearches(kibanaIndex, callCluster, rollupIndexPatternToFlagMap) { +async function fetchRollupSavedSearches( + kibanaIndex: string, + callCluster: CallCluster, + rollupIndexPatternToFlagMap: IdToFlagMap +) { const searchParams = { size: ES_MAX_RESULT_WINDOW_DEFAULT_VALUE, index: kibanaIndex, @@ -86,19 +96,19 @@ async function fetchRollupSavedSearches(kibanaIndex, callCluster, rollupIndexPat const searchSource = JSON.parse(searchSourceJSON); if (rollupIndexPatternToFlagMap[searchSource.index]) { - const id = getIdFromSavedObjectId(savedObjectId); + const id = getIdFromSavedObjectId(savedObjectId) as string; rollupSavedSearches.push(id); } return rollupSavedSearches; - }, []); + }, [] as string[]); } async function fetchRollupVisualizations( - kibanaIndex, - callCluster, - rollupIndexPatternToFlagMap, - rollupSavedSearchesToFlagMap + kibanaIndex: string, + callCluster: CallCluster, + rollupIndexPatternToFlagMap: IdToFlagMap, + rollupSavedSearchesToFlagMap: IdToFlagMap ) { const searchParams = { size: ES_MAX_RESULT_WINDOW_DEFAULT_VALUE, @@ -135,7 +145,7 @@ async function fetchRollupVisualizations( savedSearchRefName, kibanaSavedObjectMeta: { searchSourceJSON }, }, - references = [], + references = [] as any[], }, } = visualization; @@ -164,13 +174,14 @@ async function fetchRollupVisualizations( }; } -export function registerRollupUsageCollector(usageCollection, server) { - const kibanaIndex = server.config().get('kibana.index'); - +export function registerRollupUsageCollector( + usageCollection: UsageCollectionSetup, + kibanaIndex: string +): void { const collector = usageCollection.makeUsageCollector({ type: ROLLUP_USAGE_TYPE, isReady: () => true, - fetch: async callCluster => { + fetch: async (callCluster: CallCluster) => { const rollupIndexPatterns = await fetchRollupIndexPatterns(kibanaIndex, callCluster); const rollupIndexPatternToFlagMap = createIdToFlagMap(rollupIndexPatterns); diff --git a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/index.js b/x-pack/legacy/plugins/rollup/server/index.ts similarity index 55% rename from x-pack/legacy/plugins/rollup/server/lib/error_wrappers/index.js rename to x-pack/legacy/plugins/rollup/server/index.ts index f275f15637091..6bbd00ac6576e 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/index.js +++ b/x-pack/legacy/plugins/rollup/server/index.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { PluginInitializerContext } from 'src/core/server'; +import { RollupsServerPlugin } from './plugin'; -export { wrapCustomError } from './wrap_custom_error'; -export { wrapEsError } from './wrap_es_error'; -export { wrapUnknownError } from './wrap_unknown_error'; +export const plugin = (ctx: PluginInitializerContext) => new RollupsServerPlugin(ctx); diff --git a/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/call_with_request_factory.js deleted file mode 100644 index 284151d404a47..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/call_with_request_factory.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; -import { elasticsearchJsPlugin } from '../../client/elasticsearch_rollup'; - -const callWithRequest = once(server => { - const client = server.newPlatform.setup.core.elasticsearch.createClient('rollup', { - plugins: [elasticsearchJsPlugin], - }); - return (request, ...args) => client.asScoped(request).callAsCurrentUser(...args); -}); - -export const callWithRequestFactory = (server, request) => { - return (...args) => { - return callWithRequest(server)(request, ...args); - }; -}; diff --git a/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/call_with_request_factory.ts b/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/call_with_request_factory.ts new file mode 100644 index 0000000000000..883b3552a7c02 --- /dev/null +++ b/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/call_with_request_factory.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ElasticsearchServiceSetup } from 'kibana/server'; +import { once } from 'lodash'; +import { elasticsearchJsPlugin } from '../../client/elasticsearch_rollup'; + +const callWithRequest = once((elasticsearchService: ElasticsearchServiceSetup) => { + const config = { plugins: [elasticsearchJsPlugin] }; + return elasticsearchService.createClient('rollup', config); +}); + +export const callWithRequestFactory = ( + elasticsearchService: ElasticsearchServiceSetup, + request: any +) => { + return (...args: any[]) => { + return ( + callWithRequest(elasticsearchService) + .asScoped(request) + // @ts-ignore + .callAsCurrentUser(...args) + ); + }; +}; diff --git a/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/index.js b/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/index.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/index.js rename to x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/index.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/check_license/__tests__/check_license.js b/x-pack/legacy/plugins/rollup/server/lib/check_license/__tests__/check_license.js deleted file mode 100644 index 933fda01c055d..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/check_license/__tests__/check_license.js +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { set } from 'lodash'; -import { checkLicense } from '../check_license'; - -describe('check_license', function() { - let mockLicenseInfo; - beforeEach(() => (mockLicenseInfo = {})); - - describe('license information is undefined', () => { - beforeEach(() => (mockLicenseInfo = undefined)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('license information is not available', () => { - beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('license information is available', () => { - beforeEach(() => { - mockLicenseInfo.isAvailable = () => true; - set(mockLicenseInfo, 'license.getType', () => 'basic'); - }); - - describe('& license is > basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true); - }); - - it('should not set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.be(undefined); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); - - describe('& license is basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true); - }); - - it('should not set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.be(undefined); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/rollup/server/lib/check_license/check_license.js b/x-pack/legacy/plugins/rollup/server/lib/check_license/check_license.js deleted file mode 100644 index 3885a20a1f358..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/check_license/check_license.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export function checkLicense(xpackLicenseInfo) { - const pluginName = 'Rollups'; - - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - isAvailable: false, - showLinks: true, - enableLinks: false, - message: i18n.translate('xpack.rollupJobs.checkLicense.errorUnavailableMessage', { - defaultMessage: - 'You cannot use {pluginName} because license information is not available at this time.', - values: { pluginName }, - }), - }; - } - - const VALID_LICENSE_MODES = ['trial', 'basic', 'standard', 'gold', 'platinum', 'enterprise']; - - const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_LICENSE_MODES); - const isLicenseActive = xpackLicenseInfo.license.isActive(); - const licenseType = xpackLicenseInfo.license.getType(); - - // License is not valid - if (!isLicenseModeValid) { - return { - isAvailable: false, - showLinks: false, - message: i18n.translate('xpack.rollupJobs.checkLicense.errorUnsupportedMessage', { - defaultMessage: - 'Your {licenseType} license does not support {pluginName}. Please upgrade your license.', - values: { licenseType, pluginName }, - }), - }; - } - - // License is valid but not active - if (!isLicenseActive) { - return { - isAvailable: false, - showLinks: true, - enableLinks: false, - message: i18n.translate('xpack.rollupJobs.checkLicense.errorExpiredMessage', { - defaultMessage: - 'You cannot use {pluginName} because your {licenseType} license has expired', - values: { licenseType, pluginName }, - }), - }; - } - - // License is valid and active - return { - isAvailable: true, - showLinks: true, - enableLinks: true, - }; -} diff --git a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/__tests__/wrap_custom_error.js b/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/__tests__/wrap_custom_error.js deleted file mode 100644 index f9c102be7a1ff..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/__tests__/wrap_custom_error.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapCustomError } from '../wrap_custom_error'; - -describe('wrap_custom_error', () => { - describe('#wrapCustomError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const statusCode = 404; - const wrappedError = wrapCustomError(originalError, statusCode); - - expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.output.statusCode).to.equal(statusCode); - }); - }); -}); diff --git a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/__tests__/wrap_es_error.js b/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/__tests__/wrap_es_error.js deleted file mode 100644 index 8241dc4329137..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/__tests__/wrap_es_error.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapEsError } from '../wrap_es_error'; - -describe('wrap_es_error', () => { - describe('#wrapEsError', () => { - let originalError; - beforeEach(() => { - originalError = new Error('I am an error'); - originalError.statusCode = 404; - originalError.response = '{}'; - }); - - it('should return a Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - - it('should return the correct Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be(originalError.message); - }); - - it('should return the correct Boom object with custom message', () => { - const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' }); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be('No encontrado!'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/__tests__/wrap_unknown_error.js b/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/__tests__/wrap_unknown_error.js deleted file mode 100644 index 85e0b2b3033ad..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/__tests__/wrap_unknown_error.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapUnknownError } from '../wrap_unknown_error'; - -describe('wrap_unknown_error', () => { - describe('#wrapUnknownError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const wrappedError = wrapUnknownError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/wrap_custom_error.js b/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/wrap_custom_error.js deleted file mode 100644 index 3295113d38ee5..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/wrap_custom_error.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps a custom error into a Boom error response and returns it - * - * @param err Object error - * @param statusCode Error status code - * @return Object Boom error response - */ -export function wrapCustomError(err, statusCode) { - return Boom.boomify(err, { statusCode }); -} diff --git a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/wrap_es_error.js b/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/wrap_es_error.js deleted file mode 100644 index 5f4884a3f2d26..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/wrap_es_error.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -function extractCausedByChain(causedBy = {}, accumulator = []) { - const { reason, caused_by } = causedBy; // eslint-disable-line camelcase - - if (reason) { - accumulator.push(reason); - } - - // eslint-disable-next-line camelcase - if (caused_by) { - return extractCausedByChain(caused_by, accumulator); - } - - return accumulator; -} - -/** - * Wraps an error thrown by the ES JS client into a Boom error response and returns it - * - * @param err Object Error thrown by ES JS client - * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages - * @return Object Boom error response - */ -export function wrapEsError(err, statusCodeToMessageMap = {}) { - const { statusCode, response } = err; - - const { - error: { - root_cause = [], // eslint-disable-line camelcase - caused_by, // eslint-disable-line camelcase - } = {}, - } = JSON.parse(response); - - // If no custom message if specified for the error's status code, just - // wrap the error as a Boom error response and return it - if (!statusCodeToMessageMap[statusCode]) { - const boomError = Boom.boomify(err, { statusCode }); - - // The caused_by chain has the most information so use that if it's available. If not then - // settle for the root_cause. - const causedByChain = extractCausedByChain(caused_by); - const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined; - - boomError.output.payload.cause = causedByChain.length ? causedByChain : defaultCause; - return boomError; - } - - // Otherwise, use the custom message to create a Boom error response and - // return it - const message = statusCodeToMessageMap[statusCode]; - return new Boom(message, { statusCode }); -} diff --git a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/wrap_unknown_error.js b/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/wrap_unknown_error.js deleted file mode 100644 index ffd915c513362..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/wrap_unknown_error.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps an unknown error into a Boom error response and returns it - * - * @param err Object Unknown error - * @return Object Boom error response - */ -export function wrapUnknownError(err) { - return Boom.boomify(err); -} diff --git a/x-pack/legacy/plugins/rollup/server/lib/check_license/index.js b/x-pack/legacy/plugins/rollup/server/lib/is_es_error/index.ts similarity index 83% rename from x-pack/legacy/plugins/rollup/server/lib/check_license/index.js rename to x-pack/legacy/plugins/rollup/server/lib/is_es_error/index.ts index f2c070fd44b6e..a9a3c61472d8c 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/check_license/index.js +++ b/x-pack/legacy/plugins/rollup/server/lib/is_es_error/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { checkLicense } from './check_license'; +export { isEsError } from './is_es_error'; diff --git a/x-pack/legacy/plugins/rollup/server/lib/is_es_error/is_es_error.ts b/x-pack/legacy/plugins/rollup/server/lib/is_es_error/is_es_error.ts new file mode 100644 index 0000000000000..4137293cf39c0 --- /dev/null +++ b/x-pack/legacy/plugins/rollup/server/lib/is_es_error/is_es_error.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as legacyElasticsearch from 'elasticsearch'; + +const esErrorsParent = legacyElasticsearch.errors._Abstract; + +export function isEsError(err: Error) { + return err instanceof esErrorsParent; +} diff --git a/x-pack/legacy/plugins/rollup/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/legacy/plugins/rollup/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js deleted file mode 100644 index 5f2141cce9395..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { isEsErrorFactory } from '../is_es_error_factory'; -import { set } from 'lodash'; - -class MockAbstractEsError {} - -describe('is_es_error_factory', () => { - let mockServer; - let isEsError; - - beforeEach(() => { - const mockEsErrors = { - _Abstract: MockAbstractEsError, - }; - mockServer = {}; - set(mockServer, 'plugins.elasticsearch.getCluster', () => ({ errors: mockEsErrors })); - - isEsError = isEsErrorFactory(mockServer); - }); - - describe('#isEsErrorFactory', () => { - it('should return a function', () => { - expect(isEsError).to.be.a(Function); - }); - - describe('returned function', () => { - it('should return true if passed-in err is a known esError', () => { - const knownEsError = new MockAbstractEsError(); - expect(isEsError(knownEsError)).to.be(true); - }); - - it('should return false if passed-in err is not a known esError', () => { - const unknownEsError = {}; - expect(isEsError(unknownEsError)).to.be(false); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/rollup/server/lib/is_es_error_factory/is_es_error_factory.js b/x-pack/legacy/plugins/rollup/server/lib/is_es_error_factory/is_es_error_factory.js deleted file mode 100644 index 6c17554385ef8..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/is_es_error_factory/is_es_error_factory.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { memoize } from 'lodash'; - -const esErrorsFactory = memoize(server => { - return server.plugins.elasticsearch.getCluster('admin').errors; -}); - -export function isEsErrorFactory(server) { - const esErrors = esErrorsFactory(server); - return function isEsError(err) { - return err instanceof esErrors._Abstract; - }; -} diff --git a/x-pack/legacy/plugins/rollup/server/lib/jobs_compatibility.js b/x-pack/legacy/plugins/rollup/server/lib/jobs_compatibility.ts similarity index 94% rename from x-pack/legacy/plugins/rollup/server/lib/jobs_compatibility.js rename to x-pack/legacy/plugins/rollup/server/lib/jobs_compatibility.ts index 9423e7befb557..f93641e5962b7 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/jobs_compatibility.js +++ b/x-pack/legacy/plugins/rollup/server/lib/jobs_compatibility.ts @@ -37,10 +37,10 @@ export function mergeJobConfigurations(jobs = []) { throw new Error('No capabilities available'); } - const allAggs = {}; + const allAggs: { [key: string]: any } = {}; // For each job, look through all of its fields - jobs.forEach(job => { + jobs.forEach((job: { fields: { [key: string]: any } }) => { const fields = job.fields; const fieldNames = Object.keys(fields); @@ -49,7 +49,7 @@ export function mergeJobConfigurations(jobs = []) { const fieldAggs = fields[fieldName]; // Look through each field's capabilities (aggregations) - fieldAggs.forEach(agg => { + fieldAggs.forEach((agg: { agg: string; interval: string }) => { const aggName = agg.agg; const aggDoesntExist = !allAggs[aggName]; const fieldDoesntExist = allAggs[aggName] && !allAggs[aggName][fieldName]; diff --git a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js b/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js deleted file mode 100644 index a73aa96209c26..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { licensePreRoutingFactory } from '../license_pre_routing_factory'; - -describe('license_pre_routing_factory', () => { - describe('#reportingFeaturePreRoutingFactory', () => { - let mockServer; - let mockLicenseCheckResults; - - beforeEach(() => { - mockServer = { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }; - }); - - it('only instantiates one instance per server', () => { - const firstInstance = licensePreRoutingFactory(mockServer); - const secondInstance = licensePreRoutingFactory(mockServer); - - expect(firstInstance).to.be(secondInstance); - }); - - describe('isAvailable is false', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: false, - }; - }); - - it('replies with 403', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const response = licensePreRouting(); - expect(response).to.be.an(Error); - expect(response.isBoom).to.be(true); - expect(response.output.statusCode).to.be(403); - }); - }); - - describe('isAvailable is true', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: true, - }; - }); - - it('replies with nothing', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const response = licensePreRouting(); - expect(response).to.be(null); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/index.js b/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/index.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/index.js rename to x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/index.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.js b/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.js deleted file mode 100644 index 1c2c9f2b2276b..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; -import { wrapCustomError } from '../error_wrappers'; -import { PLUGIN } from '../../../common'; - -export const licensePreRoutingFactory = once(server => { - const xpackMainPlugin = server.plugins.xpack_main; - - // License checking and enable/disable logic - function licensePreRouting() { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - if (!licenseCheckResults.isAvailable) { - const error = new Error(licenseCheckResults.message); - const statusCode = 403; - const wrappedError = wrapCustomError(error, statusCode); - return wrappedError; - } else { - return null; - } - } - - return licensePreRouting; -}); diff --git a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js b/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js new file mode 100644 index 0000000000000..b6cea09e0ea3c --- /dev/null +++ b/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { licensePreRoutingFactory } from '.'; +import { + LICENSE_STATUS_VALID, + LICENSE_STATUS_INVALID, +} from '../../../../../common/constants/license_status'; +import { kibanaResponseFactory } from '../../../../../../../src/core/server'; + +describe('licensePreRoutingFactory()', () => { + let mockServer; + let mockLicenseCheckResults; + + beforeEach(() => { + mockServer = { + plugins: { + xpack_main: { + info: { + feature: () => ({ + getLicenseCheckResults: () => mockLicenseCheckResults, + }), + }, + }, + }, + }; + }); + + describe('status is invalid', () => { + beforeEach(() => { + mockLicenseCheckResults = { + status: LICENSE_STATUS_INVALID, + }; + }); + + it('replies with 403', () => { + const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => {}); + const stubRequest = {}; + const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); + expect(response.status).to.be(403); + }); + }); + + describe('status is valid', () => { + beforeEach(() => { + mockLicenseCheckResults = { + status: LICENSE_STATUS_VALID, + }; + }); + + it('replies with nothing', () => { + const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => null); + const stubRequest = {}; + const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); + expect(response).to.be(null); + }); + }); +}); diff --git a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts new file mode 100644 index 0000000000000..353510d96a00d --- /dev/null +++ b/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'src/core/server'; +import { PLUGIN } from '../../../common'; +import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; +import { ServerShim } from '../../types'; + +export const licensePreRoutingFactory = ( + server: ServerShim, + handler: RequestHandler +): RequestHandler => { + const xpackMainPlugin = server.plugins.xpack_main; + + // License checking and enable/disable logic + return function licensePreRouting( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); + const { status } = licenseCheckResults; + + if (status !== LICENSE_STATUS_VALID) { + return response.customError({ + body: { + message: licenseCheckResults.messsage, + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; +}; diff --git a/x-pack/legacy/plugins/rollup/server/lib/map_capabilities.js b/x-pack/legacy/plugins/rollup/server/lib/map_capabilities.ts similarity index 81% rename from x-pack/legacy/plugins/rollup/server/lib/map_capabilities.js rename to x-pack/legacy/plugins/rollup/server/lib/map_capabilities.ts index a365ca4c75616..e0f8af865beb4 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/map_capabilities.js +++ b/x-pack/legacy/plugins/rollup/server/lib/map_capabilities.ts @@ -6,9 +6,9 @@ import { mergeJobConfigurations } from './jobs_compatibility'; -export function getCapabilitiesForRollupIndices(indices) { +export function getCapabilitiesForRollupIndices(indices: { [key: string]: any }) { const indexNames = Object.keys(indices); - const capabilities = {}; + const capabilities = {} as { [key: string]: any }; indexNames.forEach(index => { try { diff --git a/x-pack/legacy/plugins/rollup/server/lib/merge_capabilities_with_fields.js b/x-pack/legacy/plugins/rollup/server/lib/merge_capabilities_with_fields.ts similarity index 90% rename from x-pack/legacy/plugins/rollup/server/lib/merge_capabilities_with_fields.js rename to x-pack/legacy/plugins/rollup/server/lib/merge_capabilities_with_fields.ts index 76592bf12b2e3..24abe9045aae8 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/merge_capabilities_with_fields.js +++ b/x-pack/legacy/plugins/rollup/server/lib/merge_capabilities_with_fields.ts @@ -6,13 +6,18 @@ // Merge rollup capabilities information with field information +export interface Field { + name?: string; + [key: string]: any; +} + export const mergeCapabilitiesWithFields = ( - rollupIndexCapabilities, - fieldsFromFieldCapsApi, - previousFields = [] + rollupIndexCapabilities: { [key: string]: any }, + fieldsFromFieldCapsApi: { [key: string]: any }, + previousFields: Field[] = [] ) => { const rollupFields = [...previousFields]; - const rollupFieldNames = []; + const rollupFieldNames: string[] = []; Object.keys(rollupIndexCapabilities).forEach(agg => { // Field names of the aggregation diff --git a/x-pack/legacy/plugins/rollup/server/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/plugins/rollup/server/lib/register_license_checker/register_license_checker.js deleted file mode 100644 index 5f1772800a012..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/register_license_checker/register_license_checker.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - - - - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mirrorPluginStatus } from '../../../../../server/lib/mirror_plugin_status'; -import { checkLicense } from '../check_license'; -import { PLUGIN } from '../../../common'; - -export function registerLicenseChecker(server) { - const xpackMainPlugin = server.plugins.xpack_main; - const rollupPlugin = server.plugins[PLUGIN.ID]; - - mirrorPluginStatus(xpackMainPlugin, rollupPlugin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN.ID).registerLicenseCheckResultsGenerator(checkLicense); - }); -} diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/index.js b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/index.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/index.js rename to x-pack/legacy/plugins/rollup/server/lib/search_strategies/index.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.js b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts similarity index 52% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.js rename to x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts index 8fc17252f9943..91d73cecdf401 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.js +++ b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts @@ -4,9 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { unitsMap } from '@elastic/datemath'; +import dateMath from '@elastic/datemath'; + +export type Unit = 'ms' | 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y'; export const leastCommonInterval = (num = 0, base = 0) => Math.max(Math.ceil(num / base) * base, base); -export const isCalendarInterval = ({ unit, value }) => - value === 1 && ['calendar', 'mixed'].includes(unitsMap[unit].type); + +export const isCalendarInterval = ({ unit, value }: { unit: Unit; value: number }) => { + const { unitsMap } = dateMath; + return value === 1 && ['calendar', 'mixed'].includes(unitsMap[unit].type); +}; diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.js b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.js deleted file mode 100644 index fe65a7f1f30e9..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { getRollupSearchStrategy } from './rollup_search_strategy'; -import { getRollupSearchRequest } from './rollup_search_request'; -import { getRollupSearchCapabilities } from './rollup_search_capabilities'; -import { - AbstractSearchRequest, - DefaultSearchCapabilities, - AbstractSearchStrategy, -} from '../../../../../../../src/plugins/vis_type_timeseries/server'; - -export const registerRollupSearchStrategy = kbnServer => - kbnServer.afterPluginsInit(() => { - if (!kbnServer.newPlatform.setup.plugins.metrics) { - return; - } - - const { addSearchStrategy } = kbnServer.newPlatform.setup.plugins.metrics; - - const RollupSearchRequest = getRollupSearchRequest(AbstractSearchRequest); - const RollupSearchCapabilities = getRollupSearchCapabilities(DefaultSearchCapabilities); - const RollupSearchStrategy = getRollupSearchStrategy( - AbstractSearchStrategy, - RollupSearchRequest, - RollupSearchCapabilities - ); - - addSearchStrategy(new RollupSearchStrategy(kbnServer)); - }); diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js index acd016d75f97e..d466ebd69737e 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js +++ b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js @@ -6,45 +6,22 @@ import { registerRollupSearchStrategy } from './register_rollup_search_strategy'; describe('Register Rollup Search Strategy', () => { - let kbnServer; - let metrics; + let routeDependencies; + let addSearchStrategy; beforeEach(() => { - const afterPluginsInit = jest.fn(callback => callback()); - - kbnServer = { - afterPluginsInit, - newPlatform: { - setup: { plugins: {} }, - }, - }; - - metrics = { - addSearchStrategy: jest.fn().mockName('addSearchStrategy'), - AbstractSearchRequest: jest.fn().mockName('AbstractSearchRequest'), - AbstractSearchStrategy: jest.fn().mockName('AbstractSearchStrategy'), - DefaultSearchCapabilities: jest.fn().mockName('DefaultSearchCapabilities'), + routeDependencies = { + router: jest.fn().mockName('router'), + elasticsearchService: jest.fn().mockName('elasticsearchService'), + elasticsearch: jest.fn().mockName('elasticsearch'), }; - }); - - test('should run initialization on "afterPluginsInit" hook', () => { - registerRollupSearchStrategy(kbnServer); - - expect(kbnServer.afterPluginsInit).toHaveBeenCalled(); - }); - - test('should run initialization if metrics plugin available', () => { - registerRollupSearchStrategy({ - ...kbnServer, - newPlatform: { setup: { plugins: { metrics } } }, - }); - expect(metrics.addSearchStrategy).toHaveBeenCalled(); + addSearchStrategy = jest.fn().mockName('addSearchStrategy'); }); - test('should not run initialization if metrics plugin unavailable', () => { - registerRollupSearchStrategy(kbnServer); + test('should run initialization', () => { + registerRollupSearchStrategy(routeDependencies, addSearchStrategy); - expect(metrics.addSearchStrategy).not.toHaveBeenCalled(); + expect(addSearchStrategy).toHaveBeenCalled(); }); }); diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts new file mode 100644 index 0000000000000..93c4c1b52140b --- /dev/null +++ b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { getRollupSearchStrategy } from './rollup_search_strategy'; +import { getRollupSearchRequest } from './rollup_search_request'; +import { getRollupSearchCapabilities } from './rollup_search_capabilities'; +import { + AbstractSearchRequest, + DefaultSearchCapabilities, + AbstractSearchStrategy, +} from '../../../../../../../src/plugins/vis_type_timeseries/server'; +import { RouteDependencies } from '../../types'; + +export const registerRollupSearchStrategy = ( + { elasticsearchService }: RouteDependencies, + addSearchStrategy: (searchStrategy: any) => void +) => { + const RollupSearchRequest = getRollupSearchRequest(AbstractSearchRequest); + const RollupSearchCapabilities = getRollupSearchCapabilities(DefaultSearchCapabilities); + const RollupSearchStrategy = getRollupSearchStrategy( + AbstractSearchStrategy, + RollupSearchRequest, + RollupSearchCapabilities + ); + + addSearchStrategy(new RollupSearchStrategy(elasticsearchService)); +}; diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.js b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts similarity index 82% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.js rename to x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts index b84664c765dc6..5a57129aa6039 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.js +++ b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts @@ -4,24 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ import { get, has } from 'lodash'; +import { KibanaRequest } from 'kibana/server'; import { leastCommonInterval, isCalendarInterval } from './lib/interval_helper'; -export const getRollupSearchCapabilities = DefaultSearchCapabilities => +export const getRollupSearchCapabilities = (DefaultSearchCapabilities: any) => class RollupSearchCapabilities extends DefaultSearchCapabilities { - constructor(req, fieldsCapabilities, rollupIndex) { + constructor( + req: KibanaRequest, + fieldsCapabilities: { [key: string]: any }, + rollupIndex: string + ) { super(req, fieldsCapabilities); this.rollupIndex = rollupIndex; this.availableMetrics = get(fieldsCapabilities, `${rollupIndex}.aggs`, {}); } - get dateHistogram() { + public get dateHistogram() { const [dateHistogram] = Object.values(this.availableMetrics.date_histogram); return dateHistogram; } - get defaultTimeInterval() { + public get defaultTimeInterval() { return ( this.dateHistogram.fixed_interval || this.dateHistogram.calendar_interval || @@ -34,16 +39,16 @@ export const getRollupSearchCapabilities = DefaultSearchCapabilities => ); } - get searchTimezone() { + public get searchTimezone() { return get(this.dateHistogram, 'time_zone', null); } - get whiteListedMetrics() { + public get whiteListedMetrics() { const baseRestrictions = this.createUiRestriction({ count: this.createUiRestriction(), }); - const getFields = fields => + const getFields = (fields: { [key: string]: any }) => Object.keys(fields).reduce( (acc, item) => ({ ...acc, @@ -61,20 +66,20 @@ export const getRollupSearchCapabilities = DefaultSearchCapabilities => ); } - get whiteListedGroupByFields() { + public get whiteListedGroupByFields() { return this.createUiRestriction({ everything: true, terms: has(this.availableMetrics, 'terms'), }); } - get whiteListedTimerangeModes() { + public get whiteListedTimerangeModes() { return this.createUiRestriction({ last_value: true, }); } - getValidTimeInterval(userIntervalString) { + getValidTimeInterval(userIntervalString: string) { const parsedRollupJobInterval = this.parseInterval(this.defaultTimeInterval); const inRollupJobUnit = this.convertIntervalToUnit( userIntervalString, diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.js b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts similarity index 75% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.js rename to x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts index ee8e5553c8963..7e12d5286f34c 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.js +++ b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts @@ -5,9 +5,16 @@ */ const SEARCH_METHOD = 'rollup.search'; -export const getRollupSearchRequest = AbstractSearchRequest => +interface Search { + index: string; + body: { + [key: string]: any; + }; +} + +export const getRollupSearchRequest = (AbstractSearchRequest: any) => class RollupSearchRequest extends AbstractSearchRequest { - async search(searches) { + async search(searches: Search[]) { const requests = searches.map(({ body, index }) => this.callWithRequest(SEARCH_METHOD, { body, diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.js b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts similarity index 68% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.js rename to x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts index 5cf7a3c8fd941..9d5aad2c2d3bc 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.js +++ b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts @@ -4,31 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ import { indexBy, isString } from 'lodash'; +import { ElasticsearchServiceSetup, KibanaRequest } from 'kibana/server'; import { callWithRequestFactory } from '../call_with_request_factory'; import { mergeCapabilitiesWithFields } from '../merge_capabilities_with_fields'; import { getCapabilitiesForRollupIndices } from '../map_capabilities'; const ROLLUP_INDEX_CAPABILITIES_METHOD = 'rollup.rollupIndexCapabilities'; -const getRollupIndices = rollupData => Object.keys(rollupData); +const getRollupIndices = (rollupData: { [key: string]: any[] }) => Object.keys(rollupData); -const isIndexPatternContainsWildcard = indexPattern => indexPattern.includes('*'); -const isIndexPatternValid = indexPattern => +const isIndexPatternContainsWildcard = (indexPattern: string) => indexPattern.includes('*'); +const isIndexPatternValid = (indexPattern: string) => indexPattern && isString(indexPattern) && !isIndexPatternContainsWildcard(indexPattern); export const getRollupSearchStrategy = ( - AbstractSearchStrategy, - RollupSearchRequest, - RollupSearchCapabilities + AbstractSearchStrategy: any, + RollupSearchRequest: any, + RollupSearchCapabilities: any ) => class RollupSearchStrategy extends AbstractSearchStrategy { name = 'rollup'; - constructor(server) { - super(server, callWithRequestFactory, RollupSearchRequest); + constructor(elasticsearchService: ElasticsearchServiceSetup) { + super(elasticsearchService, callWithRequestFactory, RollupSearchRequest); } - getRollupData(req, indexPattern) { + getRollupData(req: KibanaRequest, indexPattern: string) { const callWithRequest = this.getCallWithRequestInstance(req); return callWithRequest(ROLLUP_INDEX_CAPABILITIES_METHOD, { @@ -36,7 +37,7 @@ export const getRollupSearchStrategy = ( }).catch(() => Promise.resolve({})); } - async checkForViability(req, indexPattern) { + async checkForViability(req: KibanaRequest, indexPattern: string) { let isViable = false; let capabilities = null; @@ -60,7 +61,14 @@ export const getRollupSearchStrategy = ( }; } - async getFieldsForWildcard(req, indexPattern, { fieldsCapabilities, rollupIndex }) { + async getFieldsForWildcard( + req: KibanaRequest, + indexPattern: string, + { + fieldsCapabilities, + rollupIndex, + }: { fieldsCapabilities: { [key: string]: any }; rollupIndex: string } + ) { const fields = await super.getFieldsForWildcard(req, indexPattern); const fieldsFromFieldCapsApi = indexBy(fields, 'name'); const rollupIndexCapabilities = fieldsCapabilities[rollupIndex].aggs; diff --git a/x-pack/legacy/plugins/rollup/server/plugin.ts b/x-pack/legacy/plugins/rollup/server/plugin.ts new file mode 100644 index 0000000000000..52b1e31af4eb2 --- /dev/null +++ b/x-pack/legacy/plugins/rollup/server/plugin.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { CoreSetup, Plugin, PluginInitializerContext, Logger } from 'src/core/server'; +import { first } from 'rxjs/operators'; +import { i18n } from '@kbn/i18n'; + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; +import { registerLicenseChecker } from '../../../server/lib/register_license_checker'; +import { PLUGIN } from '../common'; +import { ServerShim, RouteDependencies } from './types'; + +import { + registerIndicesRoute, + registerFieldsForWildcardRoute, + registerSearchRoute, + registerJobsRoute, +} from './routes/api'; + +import { registerRollupUsageCollector } from './collectors'; + +import { rollupDataEnricher } from './rollup_data_enricher'; +import { registerRollupSearchStrategy } from './lib/search_strategies'; + +export class RollupsServerPlugin implements Plugin { + log: Logger; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.log = initializerContext.logger.get(); + } + + async setup( + { http, elasticsearch: elasticsearchService }: CoreSetup, + { + __LEGACY: serverShim, + usageCollection, + metrics, + }: { + __LEGACY: ServerShim; + usageCollection?: UsageCollectionSetup; + metrics?: VisTypeTimeseriesSetup; + } + ) { + const elasticsearch = await elasticsearchService.adminClient; + const router = http.createRouter(); + const routeDependencies: RouteDependencies = { + elasticsearch, + elasticsearchService, + router, + }; + + registerLicenseChecker( + serverShim as any, + PLUGIN.ID, + PLUGIN.getI18nName(i18n), + PLUGIN.MINIMUM_LICENSE_REQUIRED + ); + + registerIndicesRoute(routeDependencies, serverShim); + registerFieldsForWildcardRoute(routeDependencies, serverShim); + registerSearchRoute(routeDependencies, serverShim); + registerJobsRoute(routeDependencies, serverShim); + + if (usageCollection) { + this.initializerContext.config.legacy.globalConfig$ + .pipe(first()) + .toPromise() + .then(config => { + registerRollupUsageCollector(usageCollection, config.kibana.index); + }) + .catch(e => { + this.log.warn(`Registering Rollup collector failed: ${e}`); + }); + } + + if ( + serverShim.plugins.index_management && + serverShim.plugins.index_management.addIndexManagementDataEnricher + ) { + serverShim.plugins.index_management.addIndexManagementDataEnricher(rollupDataEnricher); + } + + if (metrics) { + const { addSearchStrategy } = metrics; + registerRollupSearchStrategy(routeDependencies, addSearchStrategy); + } + } + + start() {} + + stop() {} +} diff --git a/x-pack/legacy/plugins/rollup/rollup_data_enricher.js b/x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts similarity index 77% rename from x-pack/legacy/plugins/rollup/rollup_data_enricher.js rename to x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts index e92cd3b0b4fbc..7c5e160c54a31 100644 --- a/x-pack/legacy/plugins/rollup/rollup_data_enricher.js +++ b/x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts @@ -4,14 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -export const rollupDataEnricher = async (indicesList, callWithRequest) => { +interface Index { + name: string; + [key: string]: unknown; +} + +export const rollupDataEnricher = async (indicesList: Index[], callWithRequest: any) => { if (!indicesList || !indicesList.length) { return indicesList; } + const params = { path: '/_all/_rollup/data', method: 'GET', }; + try { const rollupJobData = await callWithRequest('transport.request', params); return indicesList.map(index => { @@ -22,7 +29,7 @@ export const rollupDataEnricher = async (indicesList, callWithRequest) => { }; }); } catch (e) { - //swallow exceptions and return original list + // swallow exceptions and return original list return indicesList; } }; diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/index.js b/x-pack/legacy/plugins/rollup/server/routes/api/index.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/routes/api/index.js rename to x-pack/legacy/plugins/rollup/server/routes/api/index.ts diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/index_patterns.js b/x-pack/legacy/plugins/rollup/server/routes/api/index_patterns.js deleted file mode 100644 index dfc486c030812..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/index_patterns.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import Joi from 'joi'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import indexBy from 'lodash/collection/indexBy'; -import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities'; -import { mergeCapabilitiesWithFields } from '../../lib/merge_capabilities_with_fields'; -import querystring from 'querystring'; - -/** - * Get list of fields for rollup index pattern, in the format of regular index pattern fields - */ -export function registerFieldsForWildcardRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/index_patterns/rollup/_fields_for_wildcard', - method: 'GET', - config: { - pre: [licensePreRouting], - validate: { - query: Joi.object() - .keys({ - pattern: Joi.string().required(), - meta_fields: Joi.array() - .items(Joi.string()) - .default([]), - params: Joi.object() - .keys({ - rollup_index: Joi.string().required(), - }) - .required(), - }) - .default(), - }, - }, - handler: async request => { - const { pattern, meta_fields: metaFields, params } = request.query; - - // Format call to standard index pattern `fields for wildcard` - const standardRequestQuery = querystring.stringify({ pattern, meta_fields: metaFields }); - const standardRequest = { - url: `${request.getBasePath()}/api/index_patterns/_fields_for_wildcard?${standardRequestQuery}`, - method: 'GET', - headers: request.headers, - }; - - try { - // Make call and use field information from response - const standardResponse = await server.inject(standardRequest); - const fields = standardResponse.result && standardResponse.result.fields; - - const rollupIndex = params.rollup_index; - const callWithRequest = callWithRequestFactory(server, request); - - const rollupFields = []; - const fieldsFromFieldCapsApi = indexBy(fields, 'name'); - const rollupIndexCapabilities = getCapabilitiesForRollupIndices( - await callWithRequest('rollup.rollupIndexCapabilities', { - indexPattern: rollupIndex, - }) - )[rollupIndex].aggs; - - // Keep meta fields - metaFields.forEach( - field => fieldsFromFieldCapsApi[field] && rollupFields.push(fieldsFromFieldCapsApi[field]) - ); - - const mergedRollupFields = mergeCapabilitiesWithFields( - rollupIndexCapabilities, - fieldsFromFieldCapsApi, - rollupFields - ); - - return { - fields: mergedRollupFields, - }; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - return wrapUnknownError(err); - } - }, - }); -} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/index_patterns.ts b/x-pack/legacy/plugins/rollup/server/routes/api/index_patterns.ts new file mode 100644 index 0000000000000..2516840bd9537 --- /dev/null +++ b/x-pack/legacy/plugins/rollup/server/routes/api/index_patterns.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; + +import { indexBy } from 'lodash'; +import { IndexPatternsFetcher } from '../../shared_imports'; +import { RouteDependencies, ServerShim } from '../../types'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsError } from '../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities'; +import { mergeCapabilitiesWithFields, Field } from '../../lib/merge_capabilities_with_fields'; + +const parseMetaFields = (metaFields: string | string[]) => { + let parsedFields: string[] = []; + if (typeof metaFields === 'string') { + parsedFields = JSON.parse(metaFields); + } else { + parsedFields = metaFields; + } + return parsedFields; +}; + +const getFieldsForWildcardRequest = async (context: any, request: any, response: any) => { + const { callAsCurrentUser } = context.core.elasticsearch.dataClient; + const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser); + const { pattern, meta_fields: metaFields } = request.query; + + let parsedFields: string[] = []; + try { + parsedFields = parseMetaFields(metaFields); + } catch (error) { + return response.badRequest({ + body: error, + }); + } + + try { + const fields = await indexPatterns.getFieldsForWildcard({ + pattern, + metaFields: parsedFields, + }); + + return response.ok({ + body: { fields }, + headers: { + 'content-type': 'application/json', + }, + }); + } catch (error) { + return response.notFound(); + } +}; + +/** + * Get list of fields for rollup index pattern, in the format of regular index pattern fields + */ +export function registerFieldsForWildcardRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const { params, meta_fields: metaFields } = request.query; + + try { + // Make call and use field information from response + const { payload } = await getFieldsForWildcardRequest(ctx, request, response); + const fields = payload.fields; + const parsedParams = JSON.parse(params); + const rollupIndex = parsedParams.rollup_index; + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const rollupFields: Field[] = []; + const fieldsFromFieldCapsApi: { [key: string]: any } = indexBy(fields, 'name'); + const rollupIndexCapabilities = getCapabilitiesForRollupIndices( + await callWithRequest('rollup.rollupIndexCapabilities', { + indexPattern: rollupIndex, + }) + )[rollupIndex].aggs; + // Keep meta fields + metaFields.forEach( + (field: string) => + fieldsFromFieldCapsApi[field] && rollupFields.push(fieldsFromFieldCapsApi[field]) + ); + const mergedRollupFields = mergeCapabilitiesWithFields( + rollupIndexCapabilities, + fieldsFromFieldCapsApi, + rollupFields + ); + return response.ok({ body: { fields: mergedRollupFields } }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }; + + deps.router.get( + { + path: '/api/index_patterns/rollup/_fields_for_wildcard', + validate: { + query: schema.object({ + pattern: schema.string(), + meta_fields: schema.arrayOf(schema.string(), { + defaultValue: [], + }), + params: schema.string({ + validate(value) { + try { + const params = JSON.parse(value); + const keys = Object.keys(params); + const { rollup_index: rollupIndex } = params; + + if (!rollupIndex) { + return '[request query.params]: "rollup_index" is required'; + } else if (keys.length > 1) { + const invalidParams = keys.filter(key => key !== 'rollup_index'); + return `[request query.params]: ${invalidParams.join(', ')} is not allowed`; + } + } catch (err) { + return '[request query.params]: expected JSON string'; + } + }, + }), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/indices.js b/x-pack/legacy/plugins/rollup/server/routes/api/indices.js deleted file mode 100644 index 3d1c6932575bc..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/indices.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities'; - -function isNumericField(fieldCapability) { - const numericTypes = [ - 'long', - 'integer', - 'short', - 'byte', - 'double', - 'float', - 'half_float', - 'scaled_float', - ]; - return numericTypes.some(numericType => fieldCapability[numericType] != null); -} - -export function registerIndicesRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - /** - * Returns a list of all rollup index names - */ - server.route({ - path: '/api/rollup/indices', - method: 'GET', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - try { - const data = await callWithRequest('rollup.rollupIndexCapabilities', { - indexPattern: '_all', - }); - return getCapabilitiesForRollupIndices(data); - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - return wrapUnknownError(err); - } - }, - }); - - /** - * Returns information on validity of an index pattern for creating a rollup job: - * - Does the index pattern match any indices? - * - Does the index pattern match rollup indices? - * - Which date fields, numeric fields, and keyword fields are available in the matching indices? - */ - server.route({ - path: '/api/rollup/index_pattern_validity/{indexPattern}', - method: 'GET', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const { indexPattern } = request.params; - const [fieldCapabilities, rollupIndexCapabilities] = await Promise.all([ - callWithRequest('rollup.fieldCapabilities', { indexPattern }), - callWithRequest('rollup.rollupIndexCapabilities', { indexPattern }), - ]); - - const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0; - const doesMatchRollupIndices = Object.entries(rollupIndexCapabilities).length !== 0; - - const dateFields = []; - const numericFields = []; - const keywordFields = []; - - const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields); - fieldCapabilitiesEntries.forEach(([fieldName, fieldCapability]) => { - if (fieldCapability.date) { - dateFields.push(fieldName); - return; - } - - if (isNumericField(fieldCapability)) { - numericFields.push(fieldName); - return; - } - - if (fieldCapability.keyword) { - keywordFields.push(fieldName); - } - }); - - return { - doesMatchIndices, - doesMatchRollupIndices, - dateFields, - numericFields, - keywordFields, - }; - } catch (err) { - // 404s are still valid results. - if (err.statusCode === 404) { - return { - doesMatchIndices: false, - doesMatchRollupIndices: false, - dateFields: [], - numericFields: [], - keywordFields: [], - }; - } - - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - }); -} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/indices.ts b/x-pack/legacy/plugins/rollup/server/routes/api/indices.ts new file mode 100644 index 0000000000000..e78f09a71876b --- /dev/null +++ b/x-pack/legacy/plugins/rollup/server/routes/api/indices.ts @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsError } from '../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities'; +import { API_BASE_PATH } from '../../../common'; +import { RouteDependencies, ServerShim } from '../../types'; + +type NumericField = + | 'long' + | 'integer' + | 'short' + | 'byte' + | 'scaled_float' + | 'double' + | 'float' + | 'half_float'; + +interface FieldCapability { + date?: any; + keyword?: any; + long?: any; + integer?: any; + short?: any; + byte?: any; + double?: any; + float?: any; + half_float?: any; + scaled_float?: any; +} + +interface FieldCapabilities { + fields: FieldCapability[]; +} + +function isNumericField(fieldCapability: FieldCapability) { + const numericTypes = [ + 'long', + 'integer', + 'short', + 'byte', + 'double', + 'float', + 'half_float', + 'scaled_float', + ]; + return numericTypes.some(numericType => fieldCapability[numericType as NumericField] != null); +} + +export function registerIndicesRoute(deps: RouteDependencies, legacy: ServerShim) { + const getIndicesHandler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + try { + const data = await callWithRequest('rollup.rollupIndexCapabilities', { + indexPattern: '_all', + }); + return response.ok({ body: getCapabilitiesForRollupIndices(data) }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }; + + const validateIndexPatternHandler: RequestHandler = async ( + ctx, + request, + response + ) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + try { + const { indexPattern } = request.params; + const [fieldCapabilities, rollupIndexCapabilities]: [ + FieldCapabilities, + { [key: string]: any } + ] = await Promise.all([ + callWithRequest('rollup.fieldCapabilities', { indexPattern }), + callWithRequest('rollup.rollupIndexCapabilities', { indexPattern }), + ]); + + const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0; + const doesMatchRollupIndices = Object.entries(rollupIndexCapabilities).length !== 0; + + const dateFields: string[] = []; + const numericFields: string[] = []; + const keywordFields: string[] = []; + + const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields); + + fieldCapabilitiesEntries.forEach( + ([fieldName, fieldCapability]: [string, FieldCapability]) => { + if (fieldCapability.date) { + dateFields.push(fieldName); + return; + } + + if (isNumericField(fieldCapability)) { + numericFields.push(fieldName); + return; + } + + if (fieldCapability.keyword) { + keywordFields.push(fieldName); + } + } + ); + + const body = { + doesMatchIndices, + doesMatchRollupIndices, + dateFields, + numericFields, + keywordFields, + }; + + return response.ok({ body }); + } catch (err) { + // 404s are still valid results. + if (err.statusCode === 404) { + const notFoundBody = { + doesMatchIndices: false, + doesMatchRollupIndices: false, + dateFields: [], + numericFields: [], + keywordFields: [], + }; + return response.ok({ body: notFoundBody }); + } + + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + + return response.internalError({ body: err }); + } + }; + + /** + * Returns a list of all rollup index names + */ + deps.router.get( + { + path: `${API_BASE_PATH}/indices`, + validate: false, + }, + licensePreRoutingFactory(legacy, getIndicesHandler) + ); + + /** + * Returns information on validity of an index pattern for creating a rollup job: + * - Does the index pattern match any indices? + * - Does the index pattern match rollup indices? + * - Which date fields, numeric fields, and keyword fields are available in the matching indices? + */ + deps.router.get( + { + path: `${API_BASE_PATH}/index_pattern_validity/{indexPattern}`, + validate: { + params: schema.object({ + indexPattern: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, validateIndexPatternHandler) + ); +} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.js b/x-pack/legacy/plugins/rollup/server/routes/api/jobs.js deleted file mode 100644 index 1a9a402ad6518..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.js +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../lib/is_es_error_factory'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; - -export function registerJobsRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/rollup/jobs', - method: 'GET', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - try { - const callWithRequest = callWithRequestFactory(server, request); - return await callWithRequest('rollup.jobs'); - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - }); - - server.route({ - path: '/api/rollup/create', - method: 'PUT', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - try { - const { id, ...rest } = request.payload.job; - - const callWithRequest = callWithRequestFactory(server, request); - - // Create job. - await callWithRequest('rollup.createJob', { - id, - body: rest, - }); - - // Then request the newly created job. - const results = await callWithRequest('rollup.job', { id }); - return results.jobs[0]; - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - }); - - server.route({ - path: '/api/rollup/start', - method: 'POST', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - try { - const { jobIds } = request.payload; - - const callWithRequest = callWithRequestFactory(server, request); - return await Promise.all( - jobIds.map(id => callWithRequest('rollup.startJob', { id })) - ).then(() => ({ success: true })); - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - }); - - server.route({ - path: '/api/rollup/stop', - method: 'POST', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - try { - const { jobIds } = request.payload; - // For our API integration tests we need to wait for the jobs to be stopped - // in order to be able to delete them sequencially. - const { waitForCompletion } = request.query; - const callWithRequest = callWithRequestFactory(server, request); - - const stopRollupJob = id => - callWithRequest('rollup.stopJob', { - id, - waitForCompletion: waitForCompletion === 'true', - }); - - return await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true })); - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - }); - - server.route({ - path: '/api/rollup/delete', - method: 'POST', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - try { - const { jobIds } = request.payload; - - const callWithRequest = callWithRequestFactory(server, request); - return await Promise.all( - jobIds.map(id => callWithRequest('rollup.deleteJob', { id })) - ).then(() => ({ success: true })); - } catch (err) { - // There is an issue opened on ES to handle the following error correctly - // https://github.com/elastic/elasticsearch/issues/42908 - // Until then we'll modify the response here. - if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) { - err.status = 400; - err.statusCode = 400; - err.displayName = 'Bad request'; - err.message = JSON.parse(err.response).task_failures[0].reason.reason; - } - if (isEsError(err)) { - throw wrapEsError(err); - } - - throw wrapUnknownError(err); - } - }, - }); -} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts b/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts new file mode 100644 index 0000000000000..e58bc95b9a375 --- /dev/null +++ b/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsError } from '../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +import { API_BASE_PATH } from '../../../common'; +import { RouteDependencies, ServerShim } from '../../types'; + +export function registerJobsRoute(deps: RouteDependencies, legacy: ServerShim) { + const getJobsHandler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + try { + const data = await callWithRequest('rollup.jobs'); + return response.ok({ body: data }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }; + + const createJobsHandler: RequestHandler = async (ctx, request, response) => { + try { + const { id, ...rest } = request.body.job; + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + // Create job. + await callWithRequest('rollup.createJob', { + id, + body: rest, + }); + // Then request the newly created job. + const results = await callWithRequest('rollup.job', { id }); + return response.ok({ body: results.jobs[0] }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }; + + const startJobsHandler: RequestHandler = async (ctx, request, response) => { + try { + const { jobIds } = request.body; + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + const data = await Promise.all( + jobIds.map((id: string) => callWithRequest('rollup.startJob', { id })) + ).then(() => ({ success: true })); + return response.ok({ body: data }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }; + + const stopJobsHandler: RequestHandler = async (ctx, request, response) => { + try { + const { jobIds } = request.body; + // For our API integration tests we need to wait for the jobs to be stopped + // in order to be able to delete them sequencially. + const { waitForCompletion } = request.query; + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const stopRollupJob = (id: string) => + callWithRequest('rollup.stopJob', { + id, + waitForCompletion: waitForCompletion === 'true', + }); + const data = await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true })); + return response.ok({ body: data }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }; + + const deleteJobsHandler: RequestHandler = async (ctx, request, response) => { + try { + const { jobIds } = request.body; + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const data = await Promise.all( + jobIds.map((id: string) => callWithRequest('rollup.deleteJob', { id })) + ).then(() => ({ success: true })); + return response.ok({ body: data }); + } catch (err) { + // There is an issue opened on ES to handle the following error correctly + // https://github.com/elastic/elasticsearch/issues/42908 + // Until then we'll modify the response here. + if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) { + err.status = 400; + err.statusCode = 400; + err.displayName = 'Bad request'; + err.message = JSON.parse(err.response).task_failures[0].reason.reason; + } + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }; + + deps.router.get( + { + path: `${API_BASE_PATH}/jobs`, + validate: false, + }, + licensePreRoutingFactory(legacy, getJobsHandler) + ); + + deps.router.put( + { + path: `${API_BASE_PATH}/create`, + validate: { + body: schema.object({ + job: schema.object( + { + id: schema.string(), + }, + { allowUnknowns: true } + ), + }), + }, + }, + licensePreRoutingFactory(legacy, createJobsHandler) + ); + + deps.router.post( + { + path: `${API_BASE_PATH}/start`, + validate: { + body: schema.object({ + jobIds: schema.arrayOf(schema.string()), + }), + query: schema.maybe( + schema.object({ + waitForCompletion: schema.maybe(schema.string()), + }) + ), + }, + }, + licensePreRoutingFactory(legacy, startJobsHandler) + ); + + deps.router.post( + { + path: `${API_BASE_PATH}/stop`, + validate: { + body: schema.object({ + jobIds: schema.arrayOf(schema.string()), + }), + }, + }, + licensePreRoutingFactory(legacy, stopJobsHandler) + ); + + deps.router.post( + { + path: `${API_BASE_PATH}/delete`, + validate: { + body: schema.object({ + jobIds: schema.arrayOf(schema.string()), + }), + }, + }, + licensePreRoutingFactory(legacy, deleteJobsHandler) + ); +} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/search.js b/x-pack/legacy/plugins/rollup/server/routes/api/search.js deleted file mode 100644 index 58098421f0a8f..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/search.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../lib/is_es_error_factory'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; - -export function registerSearchRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/rollup/search', - method: 'POST', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const requests = request.payload.map(({ index, query }) => - callWithRequest('rollup.search', { - index, - rest_total_hits_as_int: true, - body: query, - }) - ); - - return await Promise.all(requests); - } catch (err) { - if (isEsError(err)) { - return wrapEsError(err); - } - - return wrapUnknownError(err); - } - }, - }); -} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/search.ts b/x-pack/legacy/plugins/rollup/server/routes/api/search.ts new file mode 100644 index 0000000000000..97999a4b5ce8d --- /dev/null +++ b/x-pack/legacy/plugins/rollup/server/routes/api/search.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsError } from '../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +import { API_BASE_PATH } from '../../../common'; +import { RouteDependencies, ServerShim } from '../../types'; + +export function registerSearchRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + try { + const requests = request.body.map(({ index, query }: { index: string; query: any }) => + callWithRequest('rollup.search', { + index, + rest_total_hits_as_int: true, + body: query, + }) + ); + const data = await Promise.all(requests); + return response.ok({ body: data }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }; + + deps.router.post( + { + path: `${API_BASE_PATH}/search`, + validate: { + body: schema.arrayOf( + schema.object({ + index: schema.string(), + query: schema.any(), + }) + ), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/rollup/server/lib/register_license_checker/index.js b/x-pack/legacy/plugins/rollup/server/shared_imports.ts similarity index 75% rename from x-pack/legacy/plugins/rollup/server/lib/register_license_checker/index.js rename to x-pack/legacy/plugins/rollup/server/shared_imports.ts index 7b0f97c38d129..941610b97707f 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/register_license_checker/index.js +++ b/x-pack/legacy/plugins/rollup/server/shared_imports.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { registerLicenseChecker } from './register_license_checker'; +export { IndexPatternsFetcher } from '../../../../../src/plugins/data/server'; diff --git a/x-pack/legacy/plugins/rollup/server/types.ts b/x-pack/legacy/plugins/rollup/server/types.ts new file mode 100644 index 0000000000000..62a4841133cff --- /dev/null +++ b/x-pack/legacy/plugins/rollup/server/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'src/core/server'; +import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; + +export interface ServerShim { + plugins: { + xpack_main: XPackMainPlugin; + rollup: any; + index_management: any; + }; +} + +export interface RouteDependencies { + router: IRouter; + elasticsearchService: ElasticsearchServiceSetup; + elasticsearch: IClusterClient; +} diff --git a/x-pack/legacy/plugins/rollup/server/usage/index.js b/x-pack/legacy/plugins/rollup/server/usage/index.js deleted file mode 100644 index 9304b35aeb6c7..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/usage/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { registerRollupUsageCollector } from './collector'; diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json new file mode 100644 index 0000000000000..6ab2fc8907c0d --- /dev/null +++ b/x-pack/plugins/rollup/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "rollup", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true +} diff --git a/x-pack/plugins/rollup/server/index.ts b/x-pack/plugins/rollup/server/index.ts new file mode 100644 index 0000000000000..4056842453776 --- /dev/null +++ b/x-pack/plugins/rollup/server/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'src/core/server'; +import { RollupPlugin } from './plugin'; + +export const plugin = (initContext: PluginInitializerContext) => new RollupPlugin(initContext); + +export { RollupSetup } from './plugin'; diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts new file mode 100644 index 0000000000000..fa05b8d1307d6 --- /dev/null +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, PluginInitializerContext } from 'src/core/server'; + +export class RollupPlugin implements Plugin { + private readonly initContext: PluginInitializerContext; + + constructor(initContext: PluginInitializerContext) { + this.initContext = initContext; + } + + public setup() { + return { + __legacy: { + config: this.initContext.config, + logger: this.initContext.logger, + }, + }; + } + + public start() {} + public stop() {} +} + +export interface RollupSetup { + /** @deprecated */ + __legacy: { + config: PluginInitializerContext['config']; + logger: PluginInitializerContext['logger']; + }; +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b558e892895e7..1c2a0fc3d5ac8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10253,9 +10253,6 @@ "xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV レポート", "xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF レポート", "xpack.rollupJobs.appTitle": "ロールアップジョブ", - "xpack.rollupJobs.checkLicense.errorExpiredMessage": "{licenseType} ライセンスが期限切れのため {pluginName} を使用できません", - "xpack.rollupJobs.checkLicense.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。", - "xpack.rollupJobs.checkLicense.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。", "xpack.rollupJobs.create.backButton.label": "戻る", "xpack.rollupJobs.create.dateTypeField": "日付", "xpack.rollupJobs.create.errors.dateHistogramFieldMissing": "日付フィールドが必要です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 412e31763986c..c24f2952ef2ac 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10252,9 +10252,6 @@ "xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 报告", "xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF 报告", "xpack.rollupJobs.appTitle": "汇总/打包作业", - "xpack.rollupJobs.checkLicense.errorExpiredMessage": "您不能使用 {pluginName},因为您的 {licenseType} 许可证已过期", - "xpack.rollupJobs.checkLicense.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。", - "xpack.rollupJobs.checkLicense.errorUnsupportedMessage": "您的 {licenseType} 许可证不支持 {pluginName}。请升级您的许可。", "xpack.rollupJobs.create.backButton.label": "上一步", "xpack.rollupJobs.create.dateTypeField": "日期", "xpack.rollupJobs.create.errors.dateHistogramFieldMissing": "“日期”字段必填。", diff --git a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js index 8eb084f24c52f..be2af7cb76fd5 100644 --- a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js +++ b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js @@ -32,28 +32,42 @@ export default function({ getService }) { it('"pattern" is required', async () => { uri = `${BASE_URI}`; ({ body } = await supertest.get(uri).expect(400)); - expect(body.message).to.contain('"pattern" is required'); + expect(body.message).to.contain( + '[request query.pattern]: expected value of type [string]' + ); }); it('"params" is required', async () => { params = { pattern: 'foo' }; uri = `${BASE_URI}?${querystring.stringify(params)}`; ({ body } = await supertest.get(uri).expect(400)); - expect(body.message).to.contain('"params" is required'); + expect(body.message).to.contain( + '[request query.params]: expected value of type [string]' + ); }); - it('"params" must be an object', async () => { - params = { pattern: 'foo', params: 'bar' }; + it('"params" must be a valid JSON string', async () => { + params = { pattern: 'foo', params: 'foobarbaz' }; uri = `${BASE_URI}?${querystring.stringify(params)}`; ({ body } = await supertest.get(uri).expect(400)); - expect(body.message).to.contain('"params" must be an object'); + expect(body.message).to.contain('[request query.params]: expected JSON string'); }); - it('"params" must be an object that only accepts a "rollup_index" property', async () => { - params = { pattern: 'foo', params: JSON.stringify({ someProp: 'bar' }) }; + it('"params" requires a "rollup_index" property', async () => { + params = { pattern: 'foo', params: JSON.stringify({}) }; uri = `${BASE_URI}?${querystring.stringify(params)}`; ({ body } = await supertest.get(uri).expect(400)); - expect(body.message).to.contain('"someProp" is not allowed'); + expect(body.message).to.contain('[request query.params]: "rollup_index" is required'); + }); + + it('"params" only accepts a "rollup_index" property', async () => { + params = { + pattern: 'foo', + params: JSON.stringify({ rollup_index: 'my_index', someProp: 'bar' }), + }; + uri = `${BASE_URI}?${querystring.stringify(params)}`; + ({ body } = await supertest.get(uri).expect(400)); + expect(body.message).to.contain('[request query.params]: someProp is not allowed'); }); it('"meta_fields" must be an Array', async () => { @@ -64,7 +78,9 @@ export default function({ getService }) { }; uri = `${BASE_URI}?${querystring.stringify(params)}`; ({ body } = await supertest.get(uri).expect(400)); - expect(body.message).to.contain('"meta_fields" must be an array'); + expect(body.message).to.contain( + '[request query.meta_fields]: expected value of type [array]' + ); }); it('should return 404 the rollup index to query does not exist', async () => { @@ -73,7 +89,7 @@ export default function({ getService }) { params: JSON.stringify({ rollup_index: 'bar' }), })}`; ({ body } = await supertest.get(uri).expect(404)); - expect(body.message).to.contain('no such index [bar]'); + expect(body.message).to.contain('[index_not_found_exception] no such index [bar]'); }); }); diff --git a/x-pack/test/functional/services/machine_learning/job_table.ts b/x-pack/test/functional/services/machine_learning/job_table.ts index 153a0ac477b47..dc401ca454835 100644 --- a/x-pack/test/functional/services/machine_learning/job_table.ts +++ b/x-pack/test/functional/services/machine_learning/job_table.ts @@ -208,6 +208,15 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte expect(modelSizeStats).to.have.property('model_bytes'); delete modelSizeStats.model_bytes; + // remove categorization fields from validation until + // the ES version is updated + delete modelSizeStats.categorization_status; + delete modelSizeStats.categorized_doc_count; + delete modelSizeStats.dead_category_count; + delete modelSizeStats.frequent_category_count; + delete modelSizeStats.rare_category_count; + delete modelSizeStats.total_category_count; + expect(modelSizeStats).to.eql(expectedModelSizeStats); }