diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86be6e7e97155..c8a0717c4ff94 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) @@ -542,6 +543,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/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 03c680da8fc1b..95ae6274fec60 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -69,6 +69,9 @@ into the document when displaying it. `metrics:max_buckets`:: The maximum numbers of buckets that a single data source can return. This might arise when the user selects a short interval (for example, 1s) for a long time period (1 year). +`pageNavigation`:: The style of navigation menu for Kibana. +Choices are Individual, the legacy style where every plugin is represented in the nav, +and Grouped, a new format that bundles related plugins together in nested navigation. `query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character in a query clause. Only applies when experimental query features are enabled in the query bar. To disallow leading wildcards in Lucene queries, diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index 4d83ab67810af..0c5f5a138d58f 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -23,7 +23,7 @@ import { createMemoryHistory, History, createHashHistory } from 'history'; import { AppRouter, AppNotFound } from '../ui'; import { EitherApp, MockedMounterMap, MockedMounterTuple } from '../test_types'; -import { createRenderer, createAppMounter, createLegacyAppMounter } from './utils'; +import { createRenderer, createAppMounter, createLegacyAppMounter, getUnmounter } from './utils'; import { AppStatus } from '../types'; describe('AppContainer', () => { @@ -36,7 +36,6 @@ describe('AppContainer', () => { history.push(path); return update(); }; - const mockMountersToMounters = () => new Map([...mounters].map(([appId, { mounter }]) => [appId, mounter])); const setAppLeaveHandlerMock = () => undefined; @@ -58,7 +57,8 @@ describe('AppContainer', () => { createLegacyAppMounter('legacyApp1', jest.fn()), createAppMounter('app2', '
App 2
'), createLegacyAppMounter('baseApp:legacyApp2', jest.fn()), - createAppMounter('app3', '
App 3
', '/custom/path'), + createAppMounter('app3', '
Chromeless A
', '/chromeless-a/path'), + createAppMounter('app4', '
Chromeless B
', '/chromeless-b/path'), createAppMounter('disabledApp', '
Disabled app
'), createLegacyAppMounter('disabledLegacyApp', jest.fn()), ] as Array>); @@ -75,23 +75,24 @@ describe('AppContainer', () => { }); it('calls mount handler and returned unmount function when navigating between apps', async () => { - const dom1 = await navigate('/app/app1'); const app1 = mounters.get('app1')!; + const app2 = mounters.get('app2')!; + let dom = await navigate('/app/app1'); expect(app1.mounter.mount).toHaveBeenCalled(); - expect(dom1?.html()).toMatchInlineSnapshot(` + expect(dom?.html()).toMatchInlineSnapshot(` "
basename: /app/app1 html: App 1
" `); - const app1Unmount = await app1.mounter.mount.mock.results[0].value; - const dom2 = await navigate('/app/app2'); + const app1Unmount = await getUnmounter(app1); + dom = await navigate('/app/app2'); expect(app1Unmount).toHaveBeenCalled(); - expect(mounters.get('app2')!.mounter.mount).toHaveBeenCalled(); - expect(dom2?.html()).toMatchInlineSnapshot(` + expect(app2.mounter.mount).toHaveBeenCalled(); + expect(dom?.html()).toMatchInlineSnapshot(` "
basename: /app/app2 html:
App 2
@@ -99,6 +100,82 @@ describe('AppContainer', () => { `); }); + it('can navigate between standard application and one with custom appRoute', async () => { + const standardApp = mounters.get('app1')!; + const chromelessApp = mounters.get('app3')!; + let dom = await navigate('/app/app1'); + + expect(standardApp.mounter.mount).toHaveBeenCalled(); + expect(dom?.html()).toMatchInlineSnapshot(` + "
+ basename: /app/app1 + html: App 1 +
" + `); + + const standardAppUnmount = await getUnmounter(standardApp); + dom = await navigate('/chromeless-a/path'); + + expect(standardAppUnmount).toHaveBeenCalled(); + expect(chromelessApp.mounter.mount).toHaveBeenCalled(); + expect(dom?.html()).toMatchInlineSnapshot(` + "
+ basename: /chromeless-a/path + html:
Chromeless A
+
" + `); + + const chromelessAppUnmount = await getUnmounter(standardApp); + dom = await navigate('/app/app1'); + + expect(chromelessAppUnmount).toHaveBeenCalled(); + expect(standardApp.mounter.mount).toHaveBeenCalledTimes(2); + expect(dom?.html()).toMatchInlineSnapshot(` + "
+ basename: /app/app1 + html: App 1 +
" + `); + }); + + it('can navigate between two applications with custom appRoutes', async () => { + const chromelessAppA = mounters.get('app3')!; + const chromelessAppB = mounters.get('app4')!; + let dom = await navigate('/chromeless-a/path'); + + expect(chromelessAppA.mounter.mount).toHaveBeenCalled(); + expect(dom?.html()).toMatchInlineSnapshot(` + "
+ basename: /chromeless-a/path + html:
Chromeless A
+
" + `); + + const chromelessAppAUnmount = await getUnmounter(chromelessAppA); + dom = await navigate('/chromeless-b/path'); + + expect(chromelessAppAUnmount).toHaveBeenCalled(); + expect(chromelessAppB.mounter.mount).toHaveBeenCalled(); + expect(dom?.html()).toMatchInlineSnapshot(` + "
+ basename: /chromeless-b/path + html:
Chromeless B
+
" + `); + + const chromelessAppBUnmount = await getUnmounter(chromelessAppB); + dom = await navigate('/chromeless-a/path'); + + expect(chromelessAppBUnmount).toHaveBeenCalled(); + expect(chromelessAppA.mounter.mount).toHaveBeenCalledTimes(2); + expect(dom?.html()).toMatchInlineSnapshot(` + "
+ basename: /chromeless-a/path + html:
Chromeless A
+
" + `); + }); + it('should not mount when partial route path matches', async () => { mounters.set(...createAppMounter('spaces', '
Custom Space
', '/spaces/fake-login')); mounters.set(...createAppMounter('login', '
Login Page
', '/fake-login')); diff --git a/src/core/public/application/integration_tests/utils.tsx b/src/core/public/application/integration_tests/utils.tsx index 6367d1fa12697..4f34438fc822a 100644 --- a/src/core/public/application/integration_tests/utils.tsx +++ b/src/core/public/application/integration_tests/utils.tsx @@ -23,7 +23,7 @@ import { mount } from 'enzyme'; import { I18nProvider } from '@kbn/i18n/react'; import { App, LegacyApp, AppMountParameters } from '../types'; -import { MockedMounter, MockedMounterTuple } from '../test_types'; +import { EitherApp, MockedMounter, MockedMounterTuple, Mountable } from '../test_types'; type Dom = ReturnType | null; type Renderer = () => Dom | Promise; @@ -80,3 +80,7 @@ export const createLegacyAppMounter = ( unmount: jest.fn(), }, ]; + +export function getUnmounter(app: Mountable) { + return app.mounter.mount.mock.results[0].value; +} diff --git a/src/core/public/application/test_types.ts b/src/core/public/application/test_types.ts index 3d992cb950eb4..b822597e510cb 100644 --- a/src/core/public/application/test_types.ts +++ b/src/core/public/application/test_types.ts @@ -26,18 +26,19 @@ export type ApplicationServiceContract = PublicMethodsOf; export type EitherApp = App | LegacyApp; /** @internal */ export type MockedUnmount = jest.Mocked; + +/** @internal */ +export interface Mountable { + mounter: MockedMounter; + unmount: MockedUnmount; +} + /** @internal */ export type MockedMounter = jest.Mocked>>; /** @internal */ -export type MockedMounterTuple = [ - string, - { mounter: MockedMounter; unmount: MockedUnmount } -]; +export type MockedMounterTuple = [string, Mountable]; /** @internal */ -export type MockedMounterMap = Map< - string, - { mounter: MockedMounter; unmount: MockedUnmount } ->; +export type MockedMounterMap = Map>; /** @internal */ export type MockLifecycle< T extends keyof ApplicationService, diff --git a/src/legacy/core_plugins/data/public/actions/select_range_action.ts b/src/legacy/core_plugins/data/public/actions/select_range_action.ts index 4ea5c78a9fd2b..7e1135ca96f9e 100644 --- a/src/legacy/core_plugins/data/public/actions/select_range_action.ts +++ b/src/legacy/core_plugins/data/public/actions/select_range_action.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { - IAction, + Action, createAction, IncompatibleActionError, } from '../../../../../plugins/ui_actions/public'; @@ -55,7 +55,7 @@ async function isCompatible(context: ActionContext) { export function selectRangeAction( filterManager: FilterManager, timeFilter: TimefilterContract -): IAction { +): Action { return createAction({ type: SELECT_RANGE_ACTION, id: SELECT_RANGE_ACTION, diff --git a/src/legacy/core_plugins/data/public/actions/value_click_action.ts b/src/legacy/core_plugins/data/public/actions/value_click_action.ts index 2f622eb1eb669..1e474b8f9355c 100644 --- a/src/legacy/core_plugins/data/public/actions/value_click_action.ts +++ b/src/legacy/core_plugins/data/public/actions/value_click_action.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../../../plugins/kibana_react/public'; import { - IAction, + Action, createAction, IncompatibleActionError, } from '../../../../../plugins/ui_actions/public'; @@ -58,7 +58,7 @@ async function isCompatible(context: ActionContext) { export function valueClickAction( filterManager: FilterManager, timeFilter: TimefilterContract -): IAction { +): Action { return createAction({ type: VALUE_CLICK_ACTION, id: VALUE_CLICK_ACTION, diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index ebc470555d87c..e13e8e34eaebe 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -43,19 +43,19 @@ import { VALUE_CLICK_TRIGGER, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/embeddable/public/lib/triggers'; -import { IUiActionsSetup, IUiActionsStart } from '../../../../plugins/ui_actions/public'; +import { UiActionsSetup, UiActionsStart } from '../../../../plugins/ui_actions/public'; import { SearchSetup, SearchStart, SearchService } from './search/search_service'; export interface DataPluginSetupDependencies { data: DataPublicPluginSetup; expressions: ExpressionsSetup; - uiActions: IUiActionsSetup; + uiActions: UiActionsSetup; } export interface DataPluginStartDependencies { data: DataPublicPluginStart; - uiActions: IUiActionsStart; + uiActions: UiActionsStart; } /** diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts index f47cf52c756ac..3f877520b5bf9 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts @@ -20,7 +20,7 @@ import _ from 'lodash'; import * as Rx from 'rxjs'; import { Subscription } from 'rxjs'; import { i18n } from '@kbn/i18n'; -import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; +import { ExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { RequestAdapter, Adapters } from '../../../../../../../plugins/inspector/public'; import { esFilters, @@ -111,7 +111,7 @@ export class SearchEmbeddable extends Embeddable filterManager, }: SearchEmbeddableConfig, initialInput: SearchInput, - private readonly executeTriggerActions: TExecuteTriggerActions, + private readonly executeTriggerActions: ExecuteTriggerActions, parent?: Container ) { super( diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts index 842ef2bf9c825..15b3f2d4517ac 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts @@ -19,7 +19,7 @@ import { auto } from 'angular'; import { i18n } from '@kbn/i18n'; -import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; +import { ExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { getServices } from '../../kibana_services'; import { EmbeddableFactory, @@ -43,7 +43,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< public isEditable: () => boolean; constructor( - private readonly executeTriggerActions: TExecuteTriggerActions, + private readonly executeTriggerActions: ExecuteTriggerActions, getInjector: () => Promise, isEditable: () => boolean ) { diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 45051b7085cd8..a495b56d5e9ea 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { AppMountParameters, CoreSetup, CoreStart, Plugin } from 'kibana/public'; import angular, { auto } from 'angular'; -import { IUiActionsSetup, IUiActionsStart } from 'src/plugins/ui_actions/public'; +import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { registerFeature } from './np_ready/register_feature'; import './kibana_services'; @@ -47,13 +47,13 @@ export interface DiscoverSetup { } export type DiscoverStart = void; export interface DiscoverSetupPlugins { - uiActions: IUiActionsSetup; + uiActions: UiActionsSetup; embeddable: IEmbeddableSetup; kibanaLegacy: KibanaLegacySetup; home: HomePublicPluginSetup; } export interface DiscoverStartPlugins { - uiActions: IUiActionsStart; + uiActions: UiActionsStart; embeddable: IEmbeddableStart; navigation: NavigationStart; charts: ChartsPluginStart; diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 8b653dac19262..e300ce4a0caf8 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -18,7 +18,7 @@ */ import { IScope } from 'angular'; -import { IUiActionsStart, IUiActionsSetup } from 'src/plugins/ui_actions/public'; +import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public'; import { LegacyCoreSetup, LegacyCoreStart, App, AppMountDeprecated } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; @@ -52,7 +52,7 @@ export interface PluginsSetup { expressions: ReturnType; home: HomePublicPluginSetup; inspector: InspectorSetup; - uiActions: IUiActionsSetup; + uiActions: UiActionsSetup; navigation: NavigationPublicPluginSetup; devTools: DevToolsSetup; kibanaLegacy: KibanaLegacySetup; @@ -70,7 +70,7 @@ export interface PluginsStart { expressions: ReturnType; home: HomePublicPluginStart; inspector: InspectorStart; - uiActions: IUiActionsStart; + uiActions: UiActionsStart; navigation: NavigationPublicPluginStart; devTools: DevToolsStart; kibanaLegacy: KibanaLegacyStart; diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 38c4b280d8505..1a17caa8be20e 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -439,7 +439,7 @@ export default class BaseOptimizer { optimization: { minimizer: [ new TerserPlugin({ - parallel: this.getThreadLoaderPoolConfig().workers, + parallel: false, sourceMap: false, cache: false, extractComments: false, diff --git a/src/optimize/dynamic_dll_plugin/dll_config_model.js b/src/optimize/dynamic_dll_plugin/dll_config_model.js index 2e74cb6af86d4..9ca6071b8f515 100644 --- a/src/optimize/dynamic_dll_plugin/dll_config_model.js +++ b/src/optimize/dynamic_dll_plugin/dll_config_model.js @@ -214,16 +214,20 @@ function common(config) { return webpackMerge(generateDLL(config)); } -function optimized(config) { +function optimized() { return webpackMerge({ mode: 'production', optimization: { minimizer: [ new TerserPlugin({ - // Apply the same logic used to calculate the - // threadLoaderPool workers number to spawn - // the parallel processes on terser - parallel: config.threadLoaderPoolConfig.workers, + // NOTE: we should not enable that option for now + // Since 2.0.0 terser-webpack-plugin is using jest-worker + // to run tasks in a pool of workers. Currently it looks like + // is requiring too much memory and break on large entry points + // compilations (like this) one. Also the gain we have enabling + // that option was barely noticed. + // https://github.com/webpack-contrib/terser-webpack-plugin/issues/143 + parallel: false, sourceMap: false, cache: false, extractComments: false, @@ -250,5 +254,5 @@ export function configModel(rawConfig = {}) { return webpackMerge(common(config), unoptimized()); } - return webpackMerge(common(config), optimized(config)); + return webpackMerge(common(config), optimized()); } diff --git a/src/plugins/dashboard_embeddable_container/public/actions/expand_panel_action.tsx b/src/plugins/dashboard_embeddable_container/public/actions/expand_panel_action.tsx index 68f68f8a53bcc..edfba153b2b0b 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/expand_panel_action.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/expand_panel_action.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { IEmbeddable } from '../embeddable_plugin'; -import { IAction, IncompatibleActionError } from '../ui_actions_plugin'; +import { Action, IncompatibleActionError } from '../ui_actions_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; export const EXPAND_PANEL_ACTION = 'togglePanel'; @@ -40,7 +40,7 @@ interface ActionContext { embeddable: IEmbeddable; } -export class ExpandPanelAction implements IAction { +export class ExpandPanelAction implements Action { public readonly type = EXPAND_PANEL_ACTION; public readonly id = EXPAND_PANEL_ACTION; public order = 7; diff --git a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx index 78ce6bdc4c58f..16f611a2f1ff2 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from '../../../../core/public'; import { IEmbeddable, ViewMode, IEmbeddableStart } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; -import { IAction, IncompatibleActionError } from '../ui_actions_plugin'; +import { Action, IncompatibleActionError } from '../ui_actions_plugin'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; export const REPLACE_PANEL_ACTION = 'replacePanel'; @@ -34,7 +34,7 @@ interface ActionContext { embeddable: IEmbeddable; } -export class ReplacePanelAction implements IAction { +export class ReplacePanelAction implements Action { public readonly type = REPLACE_PANEL_ACTION; public readonly id = REPLACE_PANEL_ACTION; public order = 11; diff --git a/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx b/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx index 021a1a9d1e64a..21bd1eeac6688 100644 --- a/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx @@ -22,7 +22,7 @@ import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { RefreshInterval, TimeRange, Query, esFilters } from '../../../data/public'; import { CoreStart } from '../../../../core/public'; -import { IUiActionsStart } from '../ui_actions_plugin'; +import { UiActionsStart } from '../ui_actions_plugin'; import { Container, ContainerInput, @@ -81,7 +81,7 @@ export interface DashboardContainerOptions { inspector: InspectorStartContract; SavedObjectFinder: React.ComponentType; ExitFullScreenButton: React.ComponentType; - uiActions: IUiActionsStart; + uiActions: UiActionsStart; } export type DashboardReactContextValue = KibanaReactContextValue; diff --git a/src/plugins/dashboard_embeddable_container/public/plugin.tsx b/src/plugins/dashboard_embeddable_container/public/plugin.tsx index f9cdecc4483fc..44c9dbf2dcc4b 100644 --- a/src/plugins/dashboard_embeddable_container/public/plugin.tsx +++ b/src/plugins/dashboard_embeddable_container/public/plugin.tsx @@ -21,7 +21,7 @@ import * as React from 'react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { IUiActionsSetup, IUiActionsStart } from '../../../plugins/ui_actions/public'; +import { UiActionsSetup, UiActionsStart } from '../../../plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER, IEmbeddableSetup, IEmbeddableStart } from './embeddable_plugin'; import { ExpandPanelAction, ReplacePanelAction } from '.'; import { DashboardContainerFactory } from './embeddable/dashboard_container_factory'; @@ -34,13 +34,13 @@ import { interface SetupDependencies { embeddable: IEmbeddableSetup; - uiActions: IUiActionsSetup; + uiActions: UiActionsSetup; } interface StartDependencies { embeddable: IEmbeddableStart; inspector: InspectorStartContract; - uiActions: IUiActionsStart; + uiActions: UiActionsStart; } export type Setup = void; diff --git a/src/plugins/data/public/actions/apply_filter_action.ts b/src/plugins/data/public/actions/apply_filter_action.ts index b006889637c50..cc8bcf7679cf1 100644 --- a/src/plugins/data/public/actions/apply_filter_action.ts +++ b/src/plugins/data/public/actions/apply_filter_action.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../kibana_react/public'; -import { IAction, createAction, IncompatibleActionError } from '../../../ui_actions/public'; +import { Action, createAction, IncompatibleActionError } from '../../../ui_actions/public'; import { getOverlays, getIndexPatterns } from '../services'; import { applyFiltersPopover } from '../ui/apply_filters'; import { @@ -44,7 +44,7 @@ async function isCompatible(context: ActionContext) { export function createFilterAction( filterManager: FilterManager, timeFilter: TimefilterContract -): IAction { +): Action { return createAction({ type: GLOBAL_APPLY_FILTER_ACTION, id: GLOBAL_APPLY_FILTER_ACTION, diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index e62aba5f2713d..80646bb4668d2 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -19,7 +19,7 @@ import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { IUiActionsSetup, IUiActionsStart } from 'src/plugins/ui_actions/public'; +import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; import { FieldFormatsSetup, FieldFormatsStart } from './field_formats'; import { ISearchSetup, ISearchStart } from './search'; @@ -29,11 +29,11 @@ import { IndexPatternsContract } from './index_patterns'; import { StatefulSearchBarProps } from './ui/search_bar/create_search_bar'; export interface DataSetupDependencies { - uiActions: IUiActionsSetup; + uiActions: UiActionsSetup; } export interface DataStartDependencies { - uiActions: IUiActionsStart; + uiActions: UiActionsStart; } export interface DataPublicPluginSetup { diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 0755363c9b16b..5a1ad9957d7d7 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -21,7 +21,7 @@ export { SuggestionsComponent } from './typeahead/suggestions_component'; export { IndexPatternSelect } from './index_pattern_select'; export { FilterBar } from './filter_bar'; export { QueryStringInput } from './query_string_input/query_string_input'; -export { SearchBar, SearchBarProps } from './search_bar'; +export { SearchBar, SearchBarProps, StatefulSearchBarProps } from './search_bar'; // @internal export { diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 71d76f4db49e2..c24c20bd08fb8 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -132,6 +132,10 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) filterManager: data.query.filterManager, }); const { timeRange, refreshInterval } = useTimefilter({ + dateRangeFrom: props.dateRangeFrom, + dateRangeTo: props.dateRangeTo, + refreshInterval: props.refreshInterval, + isRefreshPaused: props.isRefreshPaused, timefilter: data.query.timefilter.timefilter, }); diff --git a/src/plugins/data/public/ui/search_bar/index.tsx b/src/plugins/data/public/ui/search_bar/index.tsx index 4aa7f5fe2b040..fbc9f4a41ebbf 100644 --- a/src/plugins/data/public/ui/search_bar/index.tsx +++ b/src/plugins/data/public/ui/search_bar/index.tsx @@ -18,3 +18,4 @@ */ export { SearchBar, SearchBarProps } from './search_bar'; +export { StatefulSearchBarProps } from './create_search_bar'; diff --git a/src/plugins/data/public/ui/search_bar/lib/use_timefilter.ts b/src/plugins/data/public/ui/search_bar/lib/use_timefilter.ts index 942902ebd7286..b56c717df4978 100644 --- a/src/plugins/data/public/ui/search_bar/lib/use_timefilter.ts +++ b/src/plugins/data/public/ui/search_bar/lib/use_timefilter.ts @@ -19,15 +19,27 @@ import { useState, useEffect } from 'react'; import { Subscription } from 'rxjs'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { DataPublicPluginStart, TimeRange, RefreshInterval } from 'src/plugins/data/public'; interface UseTimefilterProps { + dateRangeFrom?: string; + dateRangeTo?: string; + refreshInterval?: number; + isRefreshPaused?: boolean; timefilter: DataPublicPluginStart['query']['timefilter']['timefilter']; } export const useTimefilter = (props: UseTimefilterProps) => { - const [timeRange, setTimerange] = useState(props.timefilter.getTime()); - const [refreshInterval, setRefreshInterval] = useState(props.timefilter.getRefreshInterval()); + const initialTimeRange: TimeRange = { + from: props.dateRangeFrom || props.timefilter.getTime().from, + to: props.dateRangeTo || props.timefilter.getTime().to, + }; + const initialRefreshInterval: RefreshInterval = { + value: props.refreshInterval || props.timefilter.getRefreshInterval().value, + pause: props.isRefreshPaused || props.timefilter.getRefreshInterval().pause, + }; + const [timeRange, setTimerange] = useState(initialTimeRange); + const [refreshInterval, setRefreshInterval] = useState(initialRefreshInterval); useEffect(() => { const subscriptions = new Subscription(); diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 2f0cdb322912b..8d2219bc5731f 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -47,13 +47,8 @@ interface SearchBarInjectedDeps { timeHistory: TimeHistoryContract; // Filter bar onFiltersUpdated?: (filters: esFilters.Filter[]) => void; - // Date picker - dateRangeFrom?: string; - dateRangeTo?: string; // Autorefresh onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; - isRefreshPaused?: boolean; - refreshInterval?: number; } export interface SearchBarOwnProps { @@ -69,6 +64,11 @@ export interface SearchBarOwnProps { showDatePicker?: boolean; showAutoRefreshOnly?: boolean; filters?: esFilters.Filter[]; + // Date picker + isRefreshPaused?: boolean; + refreshInterval?: number; + dateRangeFrom?: string; + dateRangeTo?: string; // Query bar - should be in SearchBarInjectedDeps query?: Query; // Show when user has privileges to save diff --git a/src/plugins/embeddable/public/bootstrap.ts b/src/plugins/embeddable/public/bootstrap.ts index 1e0e7dfdb0933..3ca84549c559d 100644 --- a/src/plugins/embeddable/public/bootstrap.ts +++ b/src/plugins/embeddable/public/bootstrap.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IUiActionsSetup } from 'src/plugins/ui_actions/public'; +import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER, APPLY_FILTER_TRIGGER, @@ -33,7 +33,7 @@ import { * * @param api */ -export const bootstrap = (uiActions: IUiActionsSetup) => { +export const bootstrap = (uiActions: UiActionsSetup) => { const triggerContext = { id: CONTEXT_MENU_TRIGGER, title: 'Context menu', diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts index e2592b70397f3..f471d70e5455a 100644 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts +++ b/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { IAction, createAction, IncompatibleActionError } from '../ui_actions'; +import { Action, createAction, IncompatibleActionError } from '../ui_actions'; import { IEmbeddable, EmbeddableInput } from '../embeddables'; import { esFilters } from '../../../../../plugins/data/public'; @@ -38,7 +38,7 @@ async function isCompatible(context: ActionContext) { return Boolean(root.getInput().filters !== undefined && context.filters !== undefined); } -export function createFilterAction(): IAction { +export function createFilterAction(): Action { return createAction({ type: APPLY_FILTER_ACTION, id: APPLY_FILTER_ACTION, diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 1f3eb6355e247..767def76348c8 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { IAction } from 'src/plugins/ui_actions/public'; +import { Action } from 'src/plugins/ui_actions/public'; import { GetEmbeddableFactory, ViewMode } from '../types'; import { EmbeddableFactoryNotFoundError } from '../errors'; import { IEmbeddable } from '../embeddables'; @@ -29,7 +29,7 @@ interface ActionContext { embeddable: IEmbeddable; } -export class EditPanelAction implements IAction { +export class EditPanelAction implements Action { public readonly type = EDIT_PANEL_ACTION_ID; public readonly id = EDIT_PANEL_ACTION_ID; public order = 15; diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx index d3f9b07be4754..f604cb0c274ba 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx @@ -23,7 +23,7 @@ import React from 'react'; import { EuiLoadingChart } from '@elastic/eui'; import { Subscription } from 'rxjs'; import { CoreStart } from 'src/core/public'; -import { TGetActionsCompatibleWithTrigger } from 'src/plugins/ui_actions/public'; +import { GetActionsCompatibleWithTrigger } from 'src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { ErrorEmbeddable, IEmbeddable } from '../embeddables'; @@ -35,7 +35,7 @@ export interface EmbeddableChildPanelProps { embeddableId: string; className?: string; container: IContainer; - getActions: TGetActionsCompatibleWithTrigger; + getActions: GetActionsCompatibleWithTrigger; getEmbeddableFactory: GetEmbeddableFactory; getAllEmbeddableFactories: GetEmbeddableFactories; overlays: CoreStart['overlays']; diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 196d6f934134b..9982c632f36fb 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -25,7 +25,7 @@ import { nextTick } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { I18nProvider } from '@kbn/i18n/react'; import { CONTEXT_MENU_TRIGGER } from '../triggers'; -import { IAction, ITrigger, IUiActionsApi } from 'src/plugins/ui_actions/public'; +import { Action, UiActionsApi } from 'src/plugins/ui_actions/public'; import { Trigger, GetEmbeddableFactory, ViewMode } from '../types'; import { EmbeddableFactory, isErrorEmbeddable } from '../embeddables'; import { EmbeddablePanel } from './embeddable_panel'; @@ -44,8 +44,8 @@ import { import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks'; import { EuiBadge } from '@elastic/eui'; -const actionRegistry = new Map(); -const triggerRegistry = new Map(); +const actionRegistry = new Map(); +const triggerRegistry = new Map(); const embeddableFactories = new Map(); const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); @@ -177,7 +177,7 @@ test('HelloWorldContainer in view mode hides edit mode actions', async () => { const renderInEditModeAndOpenContextMenu = async ( embeddableInputs: any, - getActions: IUiActionsApi['getTriggerCompatibleActions'] = () => Promise.resolve([]) + getActions: UiActionsApi['getTriggerCompatibleActions'] = () => Promise.resolve([]) ) => { const inspector = inspectorPluginMock.createStartContract(); diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 234d8508bb97a..c5f4265ac3b0d 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -20,11 +20,7 @@ import { EuiContextMenuPanelDescriptor, EuiPanel, htmlIdGenerator } from '@elast import classNames from 'classnames'; import React from 'react'; import { Subscription } from 'rxjs'; -import { - buildContextMenuForActions, - TGetActionsCompatibleWithTrigger, - IAction, -} from '../ui_actions'; +import { buildContextMenuForActions, GetActionsCompatibleWithTrigger, Action } from '../ui_actions'; import { CoreStart, OverlayStart } from '../../../../../core/public'; import { toMountPoint } from '../../../../kibana_react/public'; @@ -43,7 +39,7 @@ import { CustomizePanelModal } from './panel_header/panel_actions/customize_titl interface Props { embeddable: IEmbeddable; - getActions: TGetActionsCompatibleWithTrigger; + getActions: GetActionsCompatibleWithTrigger; getEmbeddableFactory: GetEmbeddableFactory; getAllEmbeddableFactories: GetEmbeddableFactories; overlays: CoreStart['overlays']; @@ -59,7 +55,7 @@ interface State { viewMode: ViewMode; hidePanelTitles: boolean; closeContextMenu: boolean; - badges: IAction[]; + badges: Action[]; } export class EmbeddablePanel extends React.Component { @@ -91,7 +87,7 @@ export class EmbeddablePanel extends React.Component { } private async refreshBadges() { - let badges: IAction[] = await this.props.getActions(PANEL_BADGE_TRIGGER, { + let badges: Action[] = await this.props.getActions(PANEL_BADGE_TRIGGER, { embeddable: this.props.embeddable, }); if (!this.mounted) return; @@ -235,7 +231,7 @@ export class EmbeddablePanel extends React.Component { // These actions are exposed on the context menu for every embeddable, they bypass the trigger // registry. - const extraActions: Array> = [ + const extraActions: Array> = [ new CustomizePanelTitleAction(createGetUserData(this.props.overlays)), new AddPanelAction( this.props.getEmbeddableFactory, @@ -249,7 +245,7 @@ export class EmbeddablePanel extends React.Component { new EditPanelAction(this.props.getEmbeddableFactory), ]; - const sorted = actions.concat(extraActions).sort((a: IAction, b: IAction) => { + const sorted = actions.concat(extraActions).sort((a: Action, b: Action) => { const bOrder = b.order || 0; const aOrder = a.order || 0; return bOrder - aOrder; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts index 9ecc4686c21b6..2759d4575da19 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts @@ -17,7 +17,7 @@ * under the License. */ import { i18n } from '@kbn/i18n'; -import { IAction } from 'src/plugins/ui_actions/public'; +import { Action } from 'src/plugins/ui_actions/public'; import { NotificationsStart, OverlayStart } from 'src/core/public'; import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types'; import { openAddPanelFlyout } from './open_add_panel_flyout'; @@ -29,7 +29,7 @@ interface ActionContext { embeddable: IContainer; } -export class AddPanelAction implements IAction { +export class AddPanelAction implements Action { public readonly type = ADD_PANEL_ACTION_ID; public readonly id = ADD_PANEL_ACTION_ID; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts index 55aaca64d5db3..e0d34fc1f4b04 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { IAction } from 'src/plugins/ui_actions/public'; +import { Action } from 'src/plugins/ui_actions/public'; import { ViewMode } from '../../../../types'; import { IEmbeddable } from '../../../../embeddables'; @@ -30,7 +30,7 @@ interface ActionContext { embeddable: IEmbeddable; } -export class CustomizePanelTitleAction implements IAction { +export class CustomizePanelTitleAction implements Action { public readonly type = CUSTOMIZE_PANEL_ACTION_ID; public id = CUSTOMIZE_PANEL_ACTION_ID; public order = 10; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts index 8e4a43a01fc17..1433bb6f78280 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { IAction } from 'src/plugins/ui_actions/public'; +import { Action } from 'src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { IEmbeddable } from '../../../embeddables'; @@ -28,7 +28,7 @@ interface ActionContext { embeddable: IEmbeddable; } -export class InspectPanelAction implements IAction { +export class InspectPanelAction implements Action { public readonly type = INSPECT_PANEL_ACTION_ID; public readonly id = INSPECT_PANEL_ACTION_ID; public order = 10; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts index 498cd8d7136c6..ee7948f3d6a4a 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts @@ -17,7 +17,7 @@ * under the License. */ import { i18n } from '@kbn/i18n'; -import { IAction, IncompatibleActionError } from '../../../ui_actions'; +import { Action, IncompatibleActionError } from '../../../ui_actions'; import { ContainerInput, IContainer } from '../../../containers'; import { ViewMode } from '../../../types'; import { IEmbeddable } from '../../../embeddables'; @@ -38,7 +38,7 @@ function hasExpandedPanelInput( return (container as IContainer<{}, ExpandedPanelInput>).getInput().expandedPanelId !== undefined; } -export class RemovePanelAction implements IAction { +export class RemovePanelAction implements Action { public readonly type = REMOVE_PANEL_ACTION; public readonly id = REMOVE_PANEL_ACTION; public order = 5; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index eedd4ba142d82..cc0733a08dd78 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -26,7 +26,7 @@ import { } from '@elastic/eui'; import classNames from 'classnames'; import React from 'react'; -import { IAction } from 'src/plugins/ui_actions/public'; +import { Action } from 'src/plugins/ui_actions/public'; import { PanelOptionsMenu } from './panel_options_menu'; import { IEmbeddable } from '../../embeddables'; @@ -36,12 +36,12 @@ export interface PanelHeaderProps { hidePanelTitles: boolean; getActionContextMenuPanel: () => Promise; closeContextMenu: boolean; - badges: IAction[]; + badges: Action[]; embeddable: IEmbeddable; headerId?: string; } -function renderBadges(badges: IAction[], embeddable: IEmbeddable) { +function renderBadges(badges: Action[], embeddable: IEmbeddable) { return badges.map(badge => ( { +export class SayHelloAction implements Action { public readonly type = SAY_HELLO_ACTION; public readonly id = SAY_HELLO_ACTION; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card.tsx index 51640749bc2b4..a8c760f7b9497 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card.tsx @@ -22,12 +22,12 @@ import { EuiCard, EuiFlexItem, EuiFlexGroup, EuiFormRow } from '@elastic/eui'; import { Subscription } from 'rxjs'; import { EuiButton } from '@elastic/eui'; import * as Rx from 'rxjs'; -import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; +import { ExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { ContactCardEmbeddable, CONTACT_USER_TRIGGER } from './contact_card_embeddable'; interface Props { embeddable: ContactCardEmbeddable; - execTrigger: TExecuteTriggerActions; + execTrigger: ExecuteTriggerActions; } interface State { diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable.tsx index de1befd1bdc1b..48f9cd2ce516d 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable.tsx @@ -19,7 +19,7 @@ import React from 'react'; import ReactDom from 'react-dom'; import { Subscription } from 'rxjs'; -import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; +import { ExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { Container } from '../../../containers'; import { EmbeddableOutput, Embeddable, EmbeddableInput } from '../../../embeddables'; import { CONTACT_CARD_EMBEDDABLE } from './contact_card_embeddable_factory'; @@ -37,7 +37,7 @@ export interface ContactCardEmbeddableOutput extends EmbeddableOutput { } export interface ContactCardEmbeddableOptions { - execAction: TExecuteTriggerActions; + execAction: ExecuteTriggerActions; } function getFullName(input: ContactCardEmbeddableInput) { diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx index d1eea5d67fb41..838c8d7de8f12 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; +import { ExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { CoreStart } from 'src/core/public'; import { toMountPoint } from '../../../../../../kibana_react/public'; @@ -36,7 +36,7 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory, - private readonly execTrigger: TExecuteTriggerActions, + private readonly execTrigger: ExecuteTriggerActions, private readonly overlays: CoreStart['overlays'] ) { super(options); diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory.ts b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory.ts index 84806ff9cfde5..d16cd6dcd2187 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory.ts @@ -17,13 +17,13 @@ * under the License. */ -import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; +import { ExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { Container, EmbeddableFactory } from '../../..'; import { ContactCardEmbeddable, ContactCardEmbeddableInput } from './contact_card_embeddable'; import { CONTACT_CARD_EMBEDDABLE } from './contact_card_embeddable_factory'; interface SlowContactCardEmbeddableFactoryOptions { - execAction: TExecuteTriggerActions; + execAction: ExecuteTriggerActions; loadTickCount?: number; } diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx index de486598470d3..7eca9f64bf937 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx @@ -20,7 +20,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { CoreStart } from 'src/core/public'; -import { TGetActionsCompatibleWithTrigger } from 'src/plugins/ui_actions/public'; +import { GetActionsCompatibleWithTrigger } from 'src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { Container, ViewMode, ContainerInput } from '../..'; import { HelloWorldContainerComponent } from './hello_world_container_component'; @@ -45,7 +45,7 @@ interface HelloWorldContainerInput extends ContainerInput { } interface HelloWorldContainerOptions { - getActions: TGetActionsCompatibleWithTrigger; + getActions: GetActionsCompatibleWithTrigger; getEmbeddableFactory: GetEmbeddableFactory; getAllEmbeddableFactories: GetEmbeddableFactories; overlays: CoreStart['overlays']; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx index 7c0e09eff1d50..413a0914bff65 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx @@ -21,14 +21,14 @@ import { Subscription } from 'rxjs'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { CoreStart } from 'src/core/public'; -import { TGetActionsCompatibleWithTrigger } from 'src/plugins/ui_actions/public'; +import { GetActionsCompatibleWithTrigger } from 'src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { IContainer, PanelState, EmbeddableChildPanel } from '../..'; import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../types'; interface Props { container: IContainer; - getActions: TGetActionsCompatibleWithTrigger; + getActions: GetActionsCompatibleWithTrigger; getEmbeddableFactory: GetEmbeddableFactory; getAllEmbeddableFactories: GetEmbeddableFactories; overlays: CoreStart['overlays']; diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.ts index df1f4e5080031..c84fb888412e1 100644 --- a/src/plugins/embeddable/public/plugin.ts +++ b/src/plugins/embeddable/public/plugin.ts @@ -17,14 +17,14 @@ * under the License. */ -import { IUiActionsSetup } from 'src/plugins/ui_actions/public'; +import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { EmbeddableFactoryRegistry } from './types'; import { createApi, EmbeddableApi } from './api'; import { bootstrap } from './bootstrap'; export interface IEmbeddableSetupDependencies { - uiActions: IUiActionsSetup; + uiActions: UiActionsSetup; } export interface IEmbeddableSetup { diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts index 49ee795bcb98a..d9e1a75d92bf3 100644 --- a/src/plugins/embeddable/public/tests/test_plugin.ts +++ b/src/plugins/embeddable/public/tests/test_plugin.ts @@ -20,7 +20,7 @@ import { CoreSetup, CoreStart } from 'src/core/public'; // eslint-disable-next-line import { uiActionsTestPlugin } from 'src/plugins/ui_actions/public/tests'; -import { IUiActionsApi } from 'src/plugins/ui_actions/public'; +import { UiActionsApi } from 'src/plugins/ui_actions/public'; import { coreMock } from '../../../../core/public/mocks'; import { EmbeddablePublicPlugin, IEmbeddableSetup, IEmbeddableStart } from '../plugin'; @@ -30,7 +30,7 @@ export interface TestPluginReturn { coreStart: CoreStart; setup: IEmbeddableSetup; doStart: (anotherCoreStart?: CoreStart) => IEmbeddableStart; - uiActions: IUiActionsApi; + uiActions: UiActionsApi; } export const testPlugin = ( diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 4e2ea44bf7642..8e0e8b3031132 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -22,13 +22,6 @@ import { TopNavMenu } from './top_nav_menu'; import { TopNavMenuData } from './top_nav_menu_data'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -const mockTimeHistory = { - add: () => {}, - get: () => { - return []; - }, -}; - const dataShim = { ui: { SearchBar: () =>
, @@ -76,12 +69,7 @@ describe('TopNavMenu', () => { it('Should render search bar', () => { const component = shallowWithIntl( - + ); expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 849a4b033399e..cf39c82eff3ce 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -24,10 +24,9 @@ import { I18nProvider } from '@kbn/i18n/react'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; -import { SearchBarProps, DataPublicPluginStart } from '../../../data/public'; +import { StatefulSearchBarProps, DataPublicPluginStart } from '../../../data/public'; -export type TopNavMenuProps = Partial & { - appName: string; +export type TopNavMenuProps = StatefulSearchBarProps & { config?: TopNavMenuData[]; showSearchBar?: boolean; data?: DataPublicPluginStart; diff --git a/src/plugins/ui_actions/public/actions/i_action.ts b/src/plugins/ui_actions/public/actions/action.ts similarity index 97% rename from src/plugins/ui_actions/public/actions/i_action.ts rename to src/plugins/ui_actions/public/actions/action.ts index 544b66b26c974..22530f003f2cd 100644 --- a/src/plugins/ui_actions/public/actions/i_action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -19,7 +19,7 @@ import { UiComponent } from 'src/plugins/kibana_utils/common'; -export interface IAction { +export interface Action { /** * Determined the order when there is more than one action matched to a trigger. * Higher numbers are displayed first. diff --git a/src/plugins/ui_actions/public/actions/create_action.ts b/src/plugins/ui_actions/public/actions/create_action.ts index 3ac7b8cbbdec1..0cec076745334 100644 --- a/src/plugins/ui_actions/public/actions/create_action.ts +++ b/src/plugins/ui_actions/public/actions/create_action.ts @@ -17,13 +17,13 @@ * under the License. */ -import { IAction } from './i_action'; +import { Action } from './action'; export function createAction( - action: { type: string; execute: IAction['execute'] } & Partial< - IAction + action: { type: string; execute: Action['execute'] } & Partial< + Action > -): IAction { +): Action { return { getIconType: () => undefined, order: 0, diff --git a/src/plugins/ui_actions/public/actions/index.ts b/src/plugins/ui_actions/public/actions/index.ts index 67bdbed9e6785..feb9a8de62eb3 100644 --- a/src/plugins/ui_actions/public/actions/index.ts +++ b/src/plugins/ui_actions/public/actions/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { IAction } from './i_action'; +export { Action } from './action'; export { createAction } from './create_action'; diff --git a/src/plugins/ui_actions/public/actions/register_action.ts b/src/plugins/ui_actions/public/actions/register_action.ts index c8d5eddac9873..5738be63c9592 100644 --- a/src/plugins/ui_actions/public/actions/register_action.ts +++ b/src/plugins/ui_actions/public/actions/register_action.ts @@ -17,9 +17,9 @@ * under the License. */ -import { IUiActionsApiPure } from '../types'; +import { UiActionsApiPure } from '../types'; -export const registerAction: IUiActionsApiPure['registerAction'] = ({ actions }) => action => { +export const registerAction: UiActionsApiPure['registerAction'] = ({ actions }) => action => { if (actions.has(action.id)) { throw new Error(`Action [action.id = ${action.id}] already registered.`); } diff --git a/src/plugins/ui_actions/public/api.ts b/src/plugins/ui_actions/public/api.ts index 39580efd9e272..9a6fd04b14e10 100644 --- a/src/plugins/ui_actions/public/api.ts +++ b/src/plugins/ui_actions/public/api.ts @@ -18,10 +18,10 @@ */ import { - IUiActionsApi, - IUiActionsDependenciesInternal, - IUiActionsDependencies, - IUiActionsApiPure, + UiActionsApi, + UiActionsDependenciesInternal, + UiActionsDependencies, + UiActionsApiPure, } from './types'; import { attachAction } from './triggers/attach_action'; import { detachAction } from './triggers/detach_action'; @@ -32,7 +32,7 @@ import { getTriggerCompatibleActions } from './triggers/get_trigger_compatible_a import { registerAction } from './actions/register_action'; import { registerTrigger } from './triggers/register_trigger'; -export const pureApi: IUiActionsApiPure = { +export const pureApi: UiActionsApiPure = { attachAction, detachAction, executeTriggerActions, @@ -43,13 +43,13 @@ export const pureApi: IUiActionsApiPure = { registerTrigger, }; -export const createApi = (deps: IUiActionsDependencies) => { - const partialApi: Partial = {}; - const depsInternal: IUiActionsDependenciesInternal = { ...deps, api: partialApi }; +export const createApi = (deps: UiActionsDependencies) => { + const partialApi: Partial = {}; + const depsInternal: UiActionsDependenciesInternal = { ...deps, api: partialApi }; for (const [key, fn] of Object.entries(pureApi)) { (partialApi as any)[key] = fn(depsInternal); } Object.freeze(partialApi); - const api = partialApi as IUiActionsApi; + const api = partialApi as UiActionsApi; return { api, depsInternal }; }; diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index 3b76ff66f3aea..7b80a8ea830c0 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -22,7 +22,7 @@ import { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { uiToReactComponent } from '../../../kibana_react/public'; -import { IAction } from '../actions'; +import { Action } from '../actions'; /** * Transforms an array of Actions to the shape EuiContextMenuPanel expects. @@ -32,7 +32,7 @@ export async function buildContextMenuForActions({ actionContext, closeMenu, }: { - actions: Array>; + actions: Array>; actionContext: A; closeMenu: () => void; }): Promise { @@ -59,7 +59,7 @@ async function buildEuiContextMenuPanelItems({ actionContext, closeMenu, }: { - actions: Array>; + actions: Array>; actionContext: A; closeMenu: () => void; }) { @@ -95,7 +95,7 @@ function convertPanelActionToContextMenuItem({ actionContext, closeMenu, }: { - action: IAction; + action: Action; actionContext: A; closeMenu: () => void; }): EuiContextMenuPanelItemDescriptor { diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index f0d21cade422d..427dbecb7aee4 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -24,13 +24,13 @@ export function plugin(initializerContext: PluginInitializerContext) { return new UiActionsPlugin(initializerContext); } -export { IUiActionsSetup, IUiActionsStart } from './plugin'; +export { UiActionsSetup, UiActionsStart } from './plugin'; export { - IAction, - ITrigger, - IUiActionsApi, - TGetActionsCompatibleWithTrigger, - TExecuteTriggerActions, + Action, + Trigger, + UiActionsApi, + GetActionsCompatibleWithTrigger, + ExecuteTriggerActions, } from './types'; export { createAction } from './actions'; export { buildContextMenuForActions } from './context_menu'; diff --git a/src/plugins/ui_actions/public/mocks.ts b/src/plugins/ui_actions/public/mocks.ts index 4a46fa54e2ac8..273c5dcf83e81 100644 --- a/src/plugins/ui_actions/public/mocks.ts +++ b/src/plugins/ui_actions/public/mocks.ts @@ -17,13 +17,13 @@ * under the License. */ -import { IUiActionsSetup, IUiActionsStart } from '.'; +import { UiActionsSetup, UiActionsStart } from '.'; import { plugin as pluginInitializer } from '.'; // eslint-disable-next-line import { coreMock } from '../../../core/public/mocks'; -export type Setup = jest.Mocked; -export type Start = jest.Mocked; +export type Setup = jest.Mocked; +export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { diff --git a/src/plugins/ui_actions/public/plugin.ts b/src/plugins/ui_actions/public/plugin.ts index 374acaaab3999..12a9b7cbc6526 100644 --- a/src/plugins/ui_actions/public/plugin.ts +++ b/src/plugins/ui_actions/public/plugin.ts @@ -18,28 +18,28 @@ */ import { CoreStart, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/public'; -import { IUiActionsApi, IActionRegistry, ITriggerRegistry } from './types'; +import { UiActionsApi, ActionRegistry, TriggerRegistry } from './types'; import { createApi } from './api'; -export interface IUiActionsSetup { - attachAction: IUiActionsApi['attachAction']; - detachAction: IUiActionsApi['detachAction']; - registerAction: IUiActionsApi['registerAction']; - registerTrigger: IUiActionsApi['registerTrigger']; +export interface UiActionsSetup { + attachAction: UiActionsApi['attachAction']; + detachAction: UiActionsApi['detachAction']; + registerAction: UiActionsApi['registerAction']; + registerTrigger: UiActionsApi['registerTrigger']; } -export type IUiActionsStart = IUiActionsApi; +export type UiActionsStart = UiActionsApi; -export class UiActionsPlugin implements Plugin { - private readonly triggers: ITriggerRegistry = new Map(); - private readonly actions: IActionRegistry = new Map(); - private api!: IUiActionsApi; +export class UiActionsPlugin implements Plugin { + private readonly triggers: TriggerRegistry = new Map(); + private readonly actions: ActionRegistry = new Map(); + private api!: UiActionsApi; constructor(initializerContext: PluginInitializerContext) { this.api = createApi({ triggers: this.triggers, actions: this.actions }).api; } - public setup(core: CoreSetup): IUiActionsSetup { + public setup(core: CoreSetup): UiActionsSetup { return { registerTrigger: this.api.registerTrigger, registerAction: this.api.registerAction, @@ -48,7 +48,7 @@ export class UiActionsPlugin implements Plugin }; } - public start(core: CoreStart): IUiActionsStart { + public start(core: CoreStart): UiActionsStart { return this.api; } diff --git a/src/plugins/ui_actions/public/tests/helpers.ts b/src/plugins/ui_actions/public/tests/helpers.ts index abbc846736cd0..d1a4a71705a81 100644 --- a/src/plugins/ui_actions/public/tests/helpers.ts +++ b/src/plugins/ui_actions/public/tests/helpers.ts @@ -17,10 +17,10 @@ * under the License. */ -import { IUiActionsDependencies } from '../types'; +import { UiActionsDependencies } from '../types'; -export const createDeps = (): IUiActionsDependencies => { - const deps: IUiActionsDependencies = { +export const createDeps = (): UiActionsDependencies => { + const deps: UiActionsDependencies = { actions: new Map(), triggers: new Map(), }; diff --git a/src/plugins/ui_actions/public/tests/test_plugin.ts b/src/plugins/ui_actions/public/tests/test_plugin.ts index d1995262ce514..dcc42fd9f6fb2 100644 --- a/src/plugins/ui_actions/public/tests/test_plugin.ts +++ b/src/plugins/ui_actions/public/tests/test_plugin.ts @@ -18,20 +18,20 @@ */ import { CoreSetup, CoreStart } from 'src/core/public'; -import { UiActionsPlugin, IUiActionsSetup, IUiActionsStart } from '../plugin'; +import { UiActionsPlugin, UiActionsSetup, UiActionsStart } from '../plugin'; -export interface IUiActionsTestPluginReturn { +export interface UiActionsTestPluginReturn { plugin: UiActionsPlugin; coreSetup: CoreSetup; coreStart: CoreStart; - setup: IUiActionsSetup; - doStart: (anotherCoreStart?: CoreStart) => IUiActionsStart; + setup: UiActionsSetup; + doStart: (anotherCoreStart?: CoreStart) => UiActionsStart; } export const uiActionsTestPlugin = ( coreSetup: CoreSetup = {} as CoreSetup, coreStart: CoreStart = {} as CoreStart -): IUiActionsTestPluginReturn => { +): UiActionsTestPluginReturn => { const initializerContext = {} as any; const plugin = new UiActionsPlugin(initializerContext); const setup = plugin.setup(coreSetup); diff --git a/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx b/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx index 4e18b8ec27fb9..196f3e2d5cdc1 100644 --- a/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx +++ b/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiFlyout, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; import { CoreStart } from 'src/core/public'; -import { createAction, IAction } from '../../actions'; +import { createAction, Action } from '../../actions'; import { toMountPoint, reactToUiComponent } from '../../../../kibana_react/public'; const ReactMenuItem: React.FC = () => { @@ -38,7 +38,7 @@ const UiMenuItem = reactToUiComponent(ReactMenuItem); export const HELLO_WORLD_ACTION_ID = 'HELLO_WORLD_ACTION_ID'; -export function createHelloWorldAction(overlays: CoreStart['overlays']): IAction { +export function createHelloWorldAction(overlays: CoreStart['overlays']): Action { return createAction({ type: HELLO_WORLD_ACTION_ID, getIconType: () => 'lock', diff --git a/src/plugins/ui_actions/public/tests/test_samples/restricted_action.ts b/src/plugins/ui_actions/public/tests/test_samples/restricted_action.ts index 2e863b43e0917..aa65d3af98163 100644 --- a/src/plugins/ui_actions/public/tests/test_samples/restricted_action.ts +++ b/src/plugins/ui_actions/public/tests/test_samples/restricted_action.ts @@ -17,11 +17,11 @@ * under the License. */ -import { IAction, createAction } from '../../actions'; +import { Action, createAction } from '../../actions'; export const RESTRICTED_ACTION = 'RESTRICTED_ACTION'; -export function createRestrictedAction(isCompatibleIn: (context: C) => boolean): IAction { +export function createRestrictedAction(isCompatibleIn: (context: C) => boolean): Action { return createAction({ type: RESTRICTED_ACTION, isCompatible: async context => isCompatibleIn(context), diff --git a/src/plugins/ui_actions/public/tests/test_samples/say_hello_action.tsx b/src/plugins/ui_actions/public/tests/test_samples/say_hello_action.tsx index e984dd8fb64cc..f1265fed54b38 100644 --- a/src/plugins/ui_actions/public/tests/test_samples/say_hello_action.tsx +++ b/src/plugins/ui_actions/public/tests/test_samples/say_hello_action.tsx @@ -20,12 +20,12 @@ import React from 'react'; import { EuiFlyout } from '@elastic/eui'; import { CoreStart } from 'src/core/public'; -import { IAction, createAction } from '../../actions'; +import { Action, createAction } from '../../actions'; import { toMountPoint } from '../../../../kibana_react/public'; export const SAY_HELLO_ACTION = 'SAY_HELLO_ACTION'; -export function createSayHelloAction(overlays: CoreStart['overlays']): IAction<{ name: string }> { +export function createSayHelloAction(overlays: CoreStart['overlays']): Action<{ name: string }> { return createAction<{ name: string }>({ type: SAY_HELLO_ACTION, getDisplayName: ({ name }) => `Hello, ${name}`, diff --git a/src/plugins/ui_actions/public/triggers/attach_action.ts b/src/plugins/ui_actions/public/triggers/attach_action.ts index 17793d46c5a42..6c0beeae2bcd7 100644 --- a/src/plugins/ui_actions/public/triggers/attach_action.ts +++ b/src/plugins/ui_actions/public/triggers/attach_action.ts @@ -17,9 +17,9 @@ * under the License. */ -import { IUiActionsApiPure } from '../types'; +import { UiActionsApiPure } from '../types'; -export const attachAction: IUiActionsApiPure['attachAction'] = ({ triggers }) => ( +export const attachAction: UiActionsApiPure['attachAction'] = ({ triggers }) => ( triggerId, actionId ) => { diff --git a/src/plugins/ui_actions/public/triggers/detach_action.ts b/src/plugins/ui_actions/public/triggers/detach_action.ts index cb9bf685cdc00..710dcf9f5621b 100644 --- a/src/plugins/ui_actions/public/triggers/detach_action.ts +++ b/src/plugins/ui_actions/public/triggers/detach_action.ts @@ -17,9 +17,9 @@ * under the License. */ -import { IUiActionsApiPure } from '../types'; +import { UiActionsApiPure } from '../types'; -export const detachAction: IUiActionsApiPure['detachAction'] = ({ triggers }) => ( +export const detachAction: UiActionsApiPure['detachAction'] = ({ triggers }) => ( triggerId, actionId ) => { diff --git a/src/plugins/ui_actions/public/triggers/execute_trigger_actions.test.ts b/src/plugins/ui_actions/public/triggers/execute_trigger_actions.test.ts index f96a11d5f8cfe..7f2506daee268 100644 --- a/src/plugins/ui_actions/public/triggers/execute_trigger_actions.test.ts +++ b/src/plugins/ui_actions/public/triggers/execute_trigger_actions.test.ts @@ -17,9 +17,9 @@ * under the License. */ -import { IAction, createAction } from '../actions'; +import { Action, createAction } from '../actions'; import { openContextMenu } from '../context_menu'; -import { IUiActionsTestPluginReturn, uiActionsTestPlugin } from '../tests/test_plugin'; +import { UiActionsTestPluginReturn, uiActionsTestPlugin } from '../tests/test_plugin'; jest.mock('../context_menu'); @@ -28,7 +28,7 @@ const openContextMenuSpy = (openContextMenu as any) as jest.SpyInstance; const CONTACT_USER_TRIGGER = 'CONTACT_USER_TRIGGER'; -function createTestAction(id: string, checkCompatibility: (context: A) => boolean): IAction { +function createTestAction(id: string, checkCompatibility: (context: A) => boolean): Action { return createAction({ type: 'testAction', id, @@ -37,7 +37,7 @@ function createTestAction(id: string, checkCompatibility: (context: A) => boo }); } -let uiActions: IUiActionsTestPluginReturn; +let uiActions: UiActionsTestPluginReturn; const reset = () => { uiActions = uiActionsTestPlugin(); diff --git a/src/plugins/ui_actions/public/triggers/execute_trigger_actions.ts b/src/plugins/ui_actions/public/triggers/execute_trigger_actions.ts index ab938eeb9cffd..71f69eb3bdc29 100644 --- a/src/plugins/ui_actions/public/triggers/execute_trigger_actions.ts +++ b/src/plugins/ui_actions/public/triggers/execute_trigger_actions.ts @@ -17,11 +17,11 @@ * under the License. */ -import { IUiActionsApiPure } from '../types'; +import { UiActionsApiPure } from '../types'; import { buildContextMenuForActions, openContextMenu } from '../context_menu'; -import { IAction } from '../actions'; +import { Action } from '../actions'; -const executeSingleAction = async (action: IAction, actionContext: A) => { +const executeSingleAction = async (action: Action, actionContext: A) => { const href = action.getHref && action.getHref(actionContext); // TODO: Do we need a `getHref()` special case? @@ -33,9 +33,10 @@ const executeSingleAction = async (action: IAction, action await action.execute(actionContext); }; -export const executeTriggerActions: IUiActionsApiPure['executeTriggerActions'] = ({ - api, -}) => async (triggerId, actionContext) => { +export const executeTriggerActions: UiActionsApiPure['executeTriggerActions'] = ({ api }) => async ( + triggerId, + actionContext +) => { const actions = await api.getTriggerCompatibleActions!(triggerId, actionContext); if (!actions.length) { diff --git a/src/plugins/ui_actions/public/triggers/get_trigger.ts b/src/plugins/ui_actions/public/triggers/get_trigger.ts index d5e2b25ce6c19..5c96200261a90 100644 --- a/src/plugins/ui_actions/public/triggers/get_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/get_trigger.ts @@ -17,9 +17,9 @@ * under the License. */ -import { IUiActionsApiPure } from '../types'; +import { UiActionsApiPure } from '../types'; -export const getTrigger: IUiActionsApiPure['getTrigger'] = ({ triggers }) => id => { +export const getTrigger: UiActionsApiPure['getTrigger'] = ({ triggers }) => id => { const trigger = triggers.get(id); if (!trigger) { diff --git a/src/plugins/ui_actions/public/triggers/get_trigger_actions.test.ts b/src/plugins/ui_actions/public/triggers/get_trigger_actions.test.ts index 8288bf9686fad..aef4114ffb4c6 100644 --- a/src/plugins/ui_actions/public/triggers/get_trigger_actions.test.ts +++ b/src/plugins/ui_actions/public/triggers/get_trigger_actions.test.ts @@ -17,15 +17,15 @@ * under the License. */ -import { IAction } from '../actions'; +import { Action } from '../actions'; import { uiActionsTestPlugin } from '../tests/test_plugin'; -const action1: IAction = { +const action1: Action = { id: 'action1', order: 1, type: 'type1', } as any; -const action2: IAction = { +const action2: Action = { id: 'action2', order: 2, type: 'type2', diff --git a/src/plugins/ui_actions/public/triggers/get_trigger_actions.ts b/src/plugins/ui_actions/public/triggers/get_trigger_actions.ts index 31b401a863663..37d7d5534c8c1 100644 --- a/src/plugins/ui_actions/public/triggers/get_trigger_actions.ts +++ b/src/plugins/ui_actions/public/triggers/get_trigger_actions.ts @@ -17,13 +17,13 @@ * under the License. */ -import { IUiActionsApiPure } from '../types'; -import { IAction } from '../actions'; +import { UiActionsApiPure } from '../types'; +import { Action } from '../actions'; -export const getTriggerActions: IUiActionsApiPure['getTriggerActions'] = ({ +export const getTriggerActions: UiActionsApiPure['getTriggerActions'] = ({ api, actions, }) => id => { const trigger = api.getTrigger!(id); - return trigger.actionIds.map(actionId => actions.get(actionId)).filter(Boolean) as IAction[]; + return trigger.actionIds.map(actionId => actions.get(actionId)).filter(Boolean) as Action[]; }; diff --git a/src/plugins/ui_actions/public/triggers/get_trigger_compatible_actions.test.ts b/src/plugins/ui_actions/public/triggers/get_trigger_compatible_actions.test.ts index ea89ba328f406..f4d2ea48ff6b9 100644 --- a/src/plugins/ui_actions/public/triggers/get_trigger_compatible_actions.test.ts +++ b/src/plugins/ui_actions/public/triggers/get_trigger_compatible_actions.test.ts @@ -18,12 +18,12 @@ */ import { createSayHelloAction } from '../tests/test_samples/say_hello_action'; -import { IUiActionsTestPluginReturn, uiActionsTestPlugin } from '../tests/test_plugin'; +import { UiActionsTestPluginReturn, uiActionsTestPlugin } from '../tests/test_plugin'; import { createRestrictedAction, createHelloWorldAction } from '../tests/test_samples'; -import { IAction } from '../actions'; +import { Action } from '../actions'; -let action: IAction<{ name: string }>; -let uiActions: IUiActionsTestPluginReturn; +let action: Action<{ name: string }>; +let uiActions: UiActionsTestPluginReturn; beforeEach(() => { uiActions = uiActionsTestPlugin(); action = createSayHelloAction({} as any); diff --git a/src/plugins/ui_actions/public/triggers/get_trigger_compatible_actions.ts b/src/plugins/ui_actions/public/triggers/get_trigger_compatible_actions.ts index 7843b9284eb02..8be0db7561db9 100644 --- a/src/plugins/ui_actions/public/triggers/get_trigger_compatible_actions.ts +++ b/src/plugins/ui_actions/public/triggers/get_trigger_compatible_actions.ts @@ -17,15 +17,15 @@ * under the License. */ -import { IUiActionsApiPure } from '../types'; -import { IAction } from '../actions/i_action'; +import { UiActionsApiPure } from '../types'; +import { Action } from '../actions/action'; -export const getTriggerCompatibleActions: IUiActionsApiPure['getTriggerCompatibleActions'] = ({ +export const getTriggerCompatibleActions: UiActionsApiPure['getTriggerCompatibleActions'] = ({ api, }) => async (triggerId, context) => { const actions = api.getTriggerActions!(triggerId); const isCompatibles = await Promise.all(actions.map(action => action.isCompatible(context))); - return actions.reduce( + return actions.reduce( (acc, action, i) => (isCompatibles[i] ? [...acc, action] : acc), [] ); diff --git a/src/plugins/ui_actions/public/triggers/register_trigger.ts b/src/plugins/ui_actions/public/triggers/register_trigger.ts index 252513a779d2c..c9a7bb211d05a 100644 --- a/src/plugins/ui_actions/public/triggers/register_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/register_trigger.ts @@ -17,9 +17,9 @@ * under the License. */ -import { IUiActionsApiPure } from '../types'; +import { UiActionsApiPure } from '../types'; -export const registerTrigger: IUiActionsApiPure['registerTrigger'] = ({ triggers }) => trigger => { +export const registerTrigger: UiActionsApiPure['registerTrigger'] = ({ triggers }) => trigger => { if (triggers.has(trigger.id)) { throw new Error(`Trigger [trigger.id = ${trigger.id}] already registered.`); } diff --git a/src/plugins/ui_actions/public/triggers/i_trigger.ts b/src/plugins/ui_actions/public/triggers/trigger.ts similarity index 96% rename from src/plugins/ui_actions/public/triggers/i_trigger.ts rename to src/plugins/ui_actions/public/triggers/trigger.ts index 61284dc39e525..3db11953053d5 100644 --- a/src/plugins/ui_actions/public/triggers/i_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/trigger.ts @@ -17,7 +17,7 @@ * under the License. */ -export interface ITrigger { +export interface Trigger { id: string; title?: string; description?: string; diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index d7395b1b5ccf7..ed4728342b751 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -17,42 +17,42 @@ * under the License. */ -import { IAction } from './actions/i_action'; -import { ITrigger } from './triggers/i_trigger'; +import { Action } from './actions/action'; +import { Trigger } from './triggers/trigger'; -export { IAction } from './actions'; -export { ITrigger } from './triggers/i_trigger'; +export { Action } from './actions'; +export { Trigger } from './triggers/trigger'; -export type TExecuteTriggerActions = (triggerId: string, actionContext: A) => Promise; +export type ExecuteTriggerActions = (triggerId: string, actionContext: A) => Promise; -export type TGetActionsCompatibleWithTrigger = ( +export type GetActionsCompatibleWithTrigger = ( triggerId: string, context: C -) => Promise; +) => Promise; -export interface IUiActionsApi { +export interface UiActionsApi { attachAction: (triggerId: string, actionId: string) => void; detachAction: (triggerId: string, actionId: string) => void; - executeTriggerActions: TExecuteTriggerActions; - getTrigger: (id: string) => ITrigger; - getTriggerActions: (id: string) => IAction[]; - getTriggerCompatibleActions: (triggerId: string, context: C) => Promise>>; - registerAction: (action: IAction) => void; - registerTrigger: (trigger: ITrigger) => void; + executeTriggerActions: ExecuteTriggerActions; + getTrigger: (id: string) => Trigger; + getTriggerActions: (id: string) => Action[]; + getTriggerCompatibleActions: (triggerId: string, context: C) => Promise>>; + registerAction: (action: Action) => void; + registerTrigger: (trigger: Trigger) => void; } -export interface IUiActionsDependencies { - actions: IActionRegistry; - triggers: ITriggerRegistry; +export interface UiActionsDependencies { + actions: ActionRegistry; + triggers: TriggerRegistry; } -export interface IUiActionsDependenciesInternal extends IUiActionsDependencies { - api: Readonly>; +export interface UiActionsDependenciesInternal extends UiActionsDependencies { + api: Readonly>; } -export type IUiActionsApiPure = { - [K in keyof IUiActionsApi]: (deps: IUiActionsDependenciesInternal) => IUiActionsApi[K]; +export type UiActionsApiPure = { + [K in keyof UiActionsApi]: (deps: UiActionsDependenciesInternal) => UiActionsApi[K]; }; -export type ITriggerRegistry = Map; -export type IActionRegistry = Map; +export type TriggerRegistry = Map; +export type ActionRegistry = Map; diff --git a/style_guides/accessibility_guide.md b/style_guides/accessibility_guide.md deleted file mode 100644 index 28fb69ec38185..0000000000000 --- a/style_guides/accessibility_guide.md +++ /dev/null @@ -1,286 +0,0 @@ -# Accessibility (A11Y) Guide - -This document provides some technical guidelines how to prevent several common -accessibility issues. - -## Naming elements - -### `aria-label` and `aria-labelledby` - -Every element on a page will have a name, that is read out to an assistive technology -like a screen reader. This will for most elements be the content of the element. -For form elements it will be the content of the associated ` -``` - -### Don't create keyboard traps - -**TL;DR** *If you can't leave an element with Tab again, it needs a special interaction model.* - -If an interactive element consumes the Tab key (e.g. a code editor to -create an actual tabular indentation) it will prevent a keyboard user to leave -that element again. Also see [WCAG 2.1.2](https://www.w3.org/TR/WCAG20/#keyboard-operation-trapping). - -Those kind of elements, require a special interaction model. A [code editor](https://github.com/elastic/kibana/pull/13339) -could require an Enter keypress before starting editing mode, and -could leave that mode on Escape again. - -Unfortunately there is no universal solution for this problem, so be aware when creating -such elements, that would consume tabbing, to think about an accessible interaction -model. - -*Hint:* If you create that kind of interactive elements `role="application"` might -be a good `role` (also see below) for that element. It is meant for elements providing -their own interaction schemes. - -## Roles - -Each DOM element has an implicit role in the accessibility tree (that assistive technologies -use). The mapping of elements to default roles can be found in the -[Accessibility API Mappings](https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings). -You can overwrite this role via the `role` attribute on an element, and the -assistive technology will now behave the same like any other element with that role -(e.g. behave like it is a button when it has `role="button"`). - -### Landmark roles - -Some roles can be used to declare so called landmarks. These landmarks tag important -parts of a web page. Screen readers offer a quick way to jump to these -parts of the page (*landmark navigation*). - -#### role=main - -The `main` role (or equivalent using the `
` tag) declares the main part -of a page. This can be used in the landmark navigation to quickly jump to the -actual main area of the page (and skip all headers, navigations, etc.). - -#### `
` - -The `
` element, can be used to mark a region on the page, so that it -appears in the landmark navigation. The section element therefore needs to have -an *accessible name*, i.e. you should add an `aria-label`, that gives a short -title to that section of the page. - -### role=search - -**TL;DR** *Place `role="search"` neither on the `` nor the `
`, but -some `div` in between.* - -Role search can be used to mark a region as used for searching. This can be used -by assistive technologies to quickly find and navigate to this section. - -If you place it on the `input` you will overwrite the implicit `textbox` or `searchbox` -role, and as such confuse the user, since it loses it meaning as in input element. -If you place it on the `form` element you will also overwrite its role and -remove it from a quick jump navigation to all forms. - -That's why it should be placed to an `div` (or any other container) between the -`form` and the `input`. In most cases we already have a div there that you can -easily put this role to. - -**Related Links:** - -* [Where to put your search role?](http://adrianroselli.com/2015/08/where-to-put-your-search-role.html) -* Discussions about making `search` role inherit the `form` role: - [wcag/113](https://github.com/w3c/wcag/issues/113), - [html-aria/118](https://github.com/w3c/html-aria/issues/18), - [aria/85](https://github.com/w3c/aria/issues/85) diff --git a/test/functional/apps/visualize/_chart_types.ts b/test/functional/apps/visualize/_chart_types.ts index 5a3c54ccff1fc..b69ed3fff3ebb 100644 --- a/test/functional/apps/visualize/_chart_types.ts +++ b/test/functional/apps/visualize/_chart_types.ts @@ -17,6 +17,7 @@ * under the License. */ +import _ from 'lodash'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -34,7 +35,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should show the correct chart types', async function() { - const expectedChartTypes = [ + let expectedChartTypes = [ 'Area', 'Controls', 'Coordinate Map', @@ -55,13 +56,19 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { 'Vertical Bar', ]; if (!isOss) { - expectedChartTypes.push('Maps'); + expectedChartTypes.push('Maps', 'Lens'); + expectedChartTypes = _.remove(expectedChartTypes, function(n) { + return n !== 'Coordinate Map'; + }); + expectedChartTypes = _.remove(expectedChartTypes, function(n) { + return n !== 'Region Map'; + }); expectedChartTypes.sort(); } log.debug('oss= ' + isOss); // find all the chart types and make sure there all there - const chartTypes = await PageObjects.visualize.getChartTypes(); + const chartTypes = (await PageObjects.visualize.getChartTypes()).sort(); log.debug('returned chart types = ' + chartTypes); log.debug('expected chart types = ' + expectedChartTypes); expect(chartTypes).to.eql(expectedChartTypes); diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index 59498bd8413a7..ae68be3ed7987 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -311,11 +311,23 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { /** * Moves forwards in the browser history. * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#forward + * + * @return {Promise} */ public async goForward() { await driver.navigate().forward(); } + /** + * Navigates to a URL via the browser history. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#to + * + * @return {Promise} + */ + public async navigateTo(url: string) { + await driver.navigate().to(url); + } + /** * Sends a sequance of keyboard keys. For each key, this will record a pair of keyDown and keyUp actions * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#sendKeys diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx index 03870410fb334..7ce348fa2111e 100644 --- a/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx @@ -31,12 +31,6 @@ export class CorePluginChromelessPlugin return renderApp(context, params); }, }); - - return { - getGreeting() { - return 'Hello from Plugin Chromeless!'; - }, - }; } public start() {} diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx index 381590af142f7..dde58eaf44f88 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx @@ -23,12 +23,12 @@ import { GetEmbeddableFactory, GetEmbeddableFactories, } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; -import { TGetActionsCompatibleWithTrigger } from '../../../../../../../../src/plugins/ui_actions/public'; +import { GetActionsCompatibleWithTrigger } from '../../../../../../../../src/plugins/ui_actions/public'; import { DashboardContainerExample } from './dashboard_container_example'; import { Start as InspectorStartContract } from '../../../../../../../../src/plugins/inspector/public'; export interface AppProps { - getActions: TGetActionsCompatibleWithTrigger; + getActions: GetActionsCompatibleWithTrigger; getEmbeddableFactory: GetEmbeddableFactory; getAllEmbeddableFactories: GetEmbeddableFactories; overlays: CoreStart['overlays']; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx index 5cfaa1c22f4e5..0237df63351cf 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx @@ -35,10 +35,10 @@ import { import { CoreStart } from '../../../../../../../../src/core/public'; import { dashboardInput } from './dashboard_input'; import { Start as InspectorStartContract } from '../../../../../../../../src/plugins/inspector/public'; -import { TGetActionsCompatibleWithTrigger } from '../../../../../../../../src/plugins/ui_actions/public'; +import { GetActionsCompatibleWithTrigger } from '../../../../../../../../src/plugins/ui_actions/public'; interface Props { - getActions: TGetActionsCompatibleWithTrigger; + getActions: GetActionsCompatibleWithTrigger; getEmbeddableFactory: GetEmbeddableFactory; getAllEmbeddableFactories: GetEmbeddableFactories; overlays: CoreStart['overlays']; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx index bdbd0b6dbb490..2c58abba60558 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx @@ -19,7 +19,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { IUiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; +import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/public'; import { createHelloWorldAction } from '../../../../../../../src/plugins/ui_actions/public/tests/test_samples'; import { @@ -54,7 +54,7 @@ export interface SetupDependencies { interface StartDependencies { embeddable: IEmbeddableStart; - uiActions: IUiActionsStart; + uiActions: UiActionsStart; inspector: InspectorStartContract; __LEGACY: { ExitFullScreenButton: React.ComponentType; diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts index f042d466c794a..7a3fb7fa85546 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/public/sample_panel_link.ts @@ -17,10 +17,10 @@ * under the License. */ import { npStart } from 'ui/new_platform'; -import { IAction, createAction } from '../../../../../src/plugins/ui_actions/public'; +import { Action, createAction } from '../../../../../src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER } from '../../../../../src/plugins/embeddable/public'; -export const createSamplePanelLink = (): IAction => +export const createSamplePanelLink = (): Action => createAction({ type: 'samplePanelLink', getDisplayName: () => 'Sample panel Link', diff --git a/test/plugin_functional/plugins/rendering_plugin/public/plugin.tsx b/test/plugin_functional/plugins/rendering_plugin/public/plugin.tsx index 6e80b56953ca0..a4925cdb8f8df 100644 --- a/test/plugin_functional/plugins/rendering_plugin/public/plugin.tsx +++ b/test/plugin_functional/plugins/rendering_plugin/public/plugin.tsx @@ -26,13 +26,24 @@ export class RenderingPlugin implements Plugin { core.application.register({ id: 'rendering', title: 'Rendering', - appRoute: '/render', + appRoute: '/render/core', async mount(context, { element }) { render(

rendering service

, element); return () => unmountComponentAtNode(element); }, }); + + core.application.register({ + id: 'custom-app-route', + title: 'Custom App Route', + appRoute: '/custom/appRoute', + async mount(context, { element }) { + render(

Custom App Route

, element); + + return () => unmountComponentAtNode(element); + }, + }); } public start() {} diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index d0025c82a7ba5..91495c4024f3a 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -22,43 +22,59 @@ import expect from '@kbn/expect'; import '../../plugins/core_provider_plugin/types'; import { PluginFunctionalProviderContext } from '../../services'; +declare global { + interface Window { + /** + * We use this global variable to track page history changes to ensure that + * navigation is done without causing a full page reload. + */ + __RENDERING_SESSION__: string[]; + } +} + // eslint-disable-next-line import/no-default-export export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common']); + const appsMenu = getService('appsMenu'); const browser = getService('browser'); const find = getService('find'); const testSubjects = getService('testSubjects'); - function navigate(path: string) { - return browser.get(`${PageObjects.common.getHostPort()}${path}`); - } - - function getLegacyMode() { + const navigateTo = (path: string) => + browser.navigateTo(`${PageObjects.common.getHostPort()}${path}`); + const navigateToApp = async (title: string) => { + await appsMenu.clickLink(title); return browser.execute(() => { + if (!('__RENDERING_SESSION__' in window)) { + window.__RENDERING_SESSION__ = []; + } + + window.__RENDERING_SESSION__.push(window.location.pathname); + }); + }; + const getLegacyMode = () => + browser.execute(() => { return JSON.parse(document.querySelector('kbn-injected-metadata')!.getAttribute('data')!) .legacyMode; }); - } - - function getUserSettings() { - return browser.execute(() => { + const getUserSettings = () => + browser.execute(() => { return JSON.parse(document.querySelector('kbn-injected-metadata')!.getAttribute('data')!) .legacyMetadata.uiSettings.user; }); - } - - async function init() { - const loading = await testSubjects.find('kbnLoadingMessage', 5000); - - return () => find.waitForElementStale(loading); - } + const exists = (selector: string) => testSubjects.exists(selector, { timeout: 5000 }); + const findLoadingMessage = () => testSubjects.find('kbnLoadingMessage', 5000); + const getRenderingSession = () => + browser.execute(() => { + return window.__RENDERING_SESSION__; + }); describe('rendering service', () => { it('renders "core" application', async () => { - await navigate('/render/core'); + await navigateTo('/render/core'); - const [loaded, legacyMode, userSettings] = await Promise.all([ - init(), + const [loadingMessage, legacyMode, userSettings] = await Promise.all([ + findLoadingMessage(), getLegacyMode(), getUserSettings(), ]); @@ -66,16 +82,16 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(legacyMode).to.be(false); expect(userSettings).to.not.be.empty(); - await loaded(); + await find.waitForElementStale(loadingMessage); - expect(await testSubjects.exists('renderingHeader')).to.be(true); + expect(await exists('renderingHeader')).to.be(true); }); it('renders "core" application without user settings', async () => { - await navigate('/render/core?includeUserSettings=false'); + await navigateTo('/render/core?includeUserSettings=false'); - const [loaded, legacyMode, userSettings] = await Promise.all([ - init(), + const [loadingMessage, legacyMode, userSettings] = await Promise.all([ + findLoadingMessage(), getLegacyMode(), getUserSettings(), ]); @@ -83,16 +99,16 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(legacyMode).to.be(false); expect(userSettings).to.be.empty(); - await loaded(); + await find.waitForElementStale(loadingMessage); - expect(await testSubjects.exists('renderingHeader')).to.be(true); + expect(await exists('renderingHeader')).to.be(true); }); it('renders "legacy" application', async () => { - await navigate('/render/core_plugin_legacy'); + await navigateTo('/render/core_plugin_legacy'); - const [loaded, legacyMode, userSettings] = await Promise.all([ - init(), + const [loadingMessage, legacyMode, userSettings] = await Promise.all([ + findLoadingMessage(), getLegacyMode(), getUserSettings(), ]); @@ -100,17 +116,17 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(legacyMode).to.be(true); expect(userSettings).to.not.be.empty(); - await loaded(); + await find.waitForElementStale(loadingMessage); - expect(await testSubjects.exists('coreLegacyCompatH1')).to.be(true); - expect(await testSubjects.exists('renderingHeader')).to.be(false); + expect(await exists('coreLegacyCompatH1')).to.be(true); + expect(await exists('renderingHeader')).to.be(false); }); it('renders "legacy" application without user settings', async () => { - await navigate('/render/core_plugin_legacy?includeUserSettings=false'); + await navigateTo('/render/core_plugin_legacy?includeUserSettings=false'); - const [loaded, legacyMode, userSettings] = await Promise.all([ - init(), + const [loadingMessage, legacyMode, userSettings] = await Promise.all([ + findLoadingMessage(), getLegacyMode(), getUserSettings(), ]); @@ -118,10 +134,56 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(legacyMode).to.be(true); expect(userSettings).to.be.empty(); - await loaded(); + await find.waitForElementStale(loadingMessage); - expect(await testSubjects.exists('coreLegacyCompatH1')).to.be(true); - expect(await testSubjects.exists('renderingHeader')).to.be(false); + expect(await exists('coreLegacyCompatH1')).to.be(true); + expect(await exists('renderingHeader')).to.be(false); + }); + + it('navigates between standard application and one with custom appRoute', async () => { + await navigateTo('/'); + await find.waitForElementStale(await findLoadingMessage()); + + await navigateToApp('App Status'); + expect(await exists('appStatusApp')).to.be(true); + expect(await exists('renderingHeader')).to.be(false); + + await navigateToApp('Rendering'); + expect(await exists('appStatusApp')).to.be(false); + expect(await exists('renderingHeader')).to.be(true); + + await navigateToApp('App Status'); + expect(await exists('appStatusApp')).to.be(true); + expect(await exists('renderingHeader')).to.be(false); + + expect(await getRenderingSession()).to.eql([ + '/app/app_status', + '/render/core', + '/app/app_status', + ]); + }); + + it('navigates between applications with custom appRoutes', async () => { + await navigateTo('/'); + await find.waitForElementStale(await findLoadingMessage()); + + await navigateToApp('Rendering'); + expect(await exists('renderingHeader')).to.be(true); + expect(await exists('customAppRouteHeader')).to.be(false); + + await navigateToApp('Custom App Route'); + expect(await exists('customAppRouteHeader')).to.be(true); + expect(await exists('renderingHeader')).to.be(false); + + await navigateToApp('Rendering'); + expect(await exists('renderingHeader')).to.be(true); + expect(await exists('customAppRouteHeader')).to.be(false); + + expect(await getRenderingSession()).to.eql([ + '/render/core', + '/custom/appRoute', + '/render/core', + ]); }); }); } diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index 4de45fe96a400..eb9df042f9254 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -23,6 +23,7 @@ Table of Contents - [`DELETE /api/alert/{id}`: Delete alert](#delete-apialertid-delete-alert) - [`GET /api/alert/_find`: Find alerts](#get-apialertfind-find-alerts) - [`GET /api/alert/{id}`: Get alert](#get-apialertid-get-alert) + - [`GET /api/alert/{id}/state`: Get alert state](#get-apialertidstate-get-alert-state) - [`GET /api/alert/types`: List alert types](#get-apialerttypes-list-alert-types) - [`PUT /api/alert/{id}`: Update alert](#put-apialertid-update-alert) - [`POST /api/alert/{id}/_enable`: Enable an alert](#post-apialertidenable-enable-an-alert) @@ -273,6 +274,14 @@ Params: |---|---|---| |id|The id of the alert you're trying to get.|string| +### `GET /api/alert/{id}/state`: Get alert state + +Params: + +|Property|Description|Type| +|---|---|---| +|id|The id of the alert whose state you're trying to get.|string| + ### `GET /api/alert/types`: List alert types No parameters. diff --git a/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.test.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.test.ts index 6a80f4d2de4cb..c5f93edfb74e5 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.test.ts @@ -192,7 +192,7 @@ describe('updateLastScheduledActions()', () => { state: {}, meta: { lastScheduledActions: { - date: new Date(), + date: new Date().toISOString(), group: 'default', }, }, @@ -216,3 +216,19 @@ describe('toJSON', () => { ); }); }); + +describe('toRaw', () => { + test('returns unserialised underlying state and meta', () => { + const raw = { + state: { foo: true }, + meta: { + lastScheduledActions: { + date: new Date(), + group: 'default', + }, + }, + }; + const alertInstance = new AlertInstance(raw); + expect(alertInstance.toRaw()).toEqual(raw); + }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts index a56e2077cdfd8..df67f7d2a1d9e 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_instance/alert_instance.ts @@ -3,34 +3,41 @@ * 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 t from 'io-ts'; import { State, Context } from '../types'; +import { DateFromString } from '../lib/types'; import { parseDuration } from '../lib'; -interface Meta { - lastScheduledActions?: { - group: string; - date: Date; - }; -} - interface ScheduledExecutionOptions { actionGroup: string; context: Context; state: State; } -interface ConstructorOptions { - state?: State; - meta?: Meta; -} +const metaSchema = t.partial({ + lastScheduledActions: t.type({ + group: t.string, + date: DateFromString, + }), +}); +type AlertInstanceMeta = t.TypeOf; + +const stateSchema = t.record(t.string, t.unknown); +type AlertInstanceState = t.TypeOf; + +export const rawAlertInstance = t.partial({ + state: stateSchema, + meta: metaSchema, +}); +export type RawAlertInstance = t.TypeOf; export class AlertInstance { private scheduledExecutionOptions?: ScheduledExecutionOptions; - private meta: Meta; - private state: State; + private meta: AlertInstanceMeta; + private state: AlertInstanceState; - constructor({ state = {}, meta = {} }: ConstructorOptions = {}) { + constructor({ state = {}, meta = {} }: RawAlertInstance = {}) { this.state = state; this.meta = meta; } @@ -48,7 +55,7 @@ export class AlertInstance { if ( this.meta.lastScheduledActions && this.meta.lastScheduledActions.group === actionGroup && - new Date(this.meta.lastScheduledActions.date).getTime() + throttleMills > Date.now() + this.meta.lastScheduledActions.date.getTime() + throttleMills > Date.now() ) { return true; } @@ -89,6 +96,10 @@ export class AlertInstance { * Used to serialize alert instance state */ toJSON() { + return rawAlertInstance.encode(this.toRaw()); + } + + toRaw(): RawAlertInstance { return { state: this.state, meta: this.meta, diff --git a/x-pack/legacy/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts index 914f726ebbd78..03bc8b7cc3b14 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_instance/create_alert_instance_factory.test.ts @@ -40,7 +40,7 @@ test('reuses existing instances', () => { Object { "meta": Object { "lastScheduledActions": Object { - "date": 1970-01-01T00:00:00.000Z, + "date": "1970-01-01T00:00:00.000Z", "group": "default", }, }, diff --git a/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts b/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts index 40ee0874e805c..fc828096adf28 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_instance/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { AlertInstance } from './alert_instance'; +export { AlertInstance, RawAlertInstance, rawAlertInstance } from './alert_instance'; export { createAlertInstanceFactory } from './create_alert_instance_factory'; diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts index c7d359491680f..3189fa214d5f7 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.mock.ts @@ -12,6 +12,7 @@ const createAlertsClientMock = () => { const mocked: jest.Mocked = { create: jest.fn(), get: jest.fn(), + getAlertState: jest.fn(), find: jest.fn(), delete: jest.fn(), update: jest.fn(), diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 56ccf08d6a44f..f9d1d97a521fe 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -1356,6 +1356,120 @@ describe('get()', () => { }); }); +describe('getAlertState()', () => { + test('calls saved objects client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + + taskManager.get.mockResolvedValueOnce({ + id: '1', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + + await alertsClient.getAlertState({ id: '1' }); + expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + ] + `); + }); + + test('gets the underlying task from TaskManager', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + + const scheduledTaskId = 'task-123'; + + savedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + enabled: true, + scheduledTaskId, + mutedInstanceIds: [], + muteAll: true, + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + + taskManager.get.mockResolvedValueOnce({ + id: scheduledTaskId, + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: { + alertId: '1', + }, + ownerId: null, + }); + + await alertsClient.getAlertState({ id: '1' }); + expect(taskManager.get).toHaveBeenCalledTimes(1); + expect(taskManager.get).toHaveBeenCalledWith(scheduledTaskId); + }); +}); + describe('find()', () => { test('calls saved objects client with given params', async () => { const alertsClient = new AlertsClient(alertsClientParams); diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index 40125f3067ee3..f6841ed5a0e46 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -31,6 +31,7 @@ import { } from '../../../../plugins/security/server'; import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../../plugins/task_manager/server'; +import { AlertTaskState, taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; type NormalizedAlertAction = Omit; export type CreateAPIKeyResult = @@ -204,6 +205,17 @@ export class AlertsClient { return this.getAlertFromRaw(result.id, result.attributes, result.updated_at, result.references); } + public async getAlertState({ id }: { id: string }): Promise { + const alert = await this.get({ id }); + if (alert.scheduledTaskId) { + const { state } = taskInstanceToAlertTaskInstance( + await this.taskManager.get(alert.scheduledTaskId), + alert + ); + return state; + } + } + public async find({ options = {} }: FindOptions = {}): Promise { const { page, diff --git a/x-pack/legacy/plugins/alerting/server/lib/types.test.ts b/x-pack/legacy/plugins/alerting/server/lib/types.test.ts new file mode 100644 index 0000000000000..517b66aa2faab --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/lib/types.test.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 { DateFromString } from './types'; +import { right, isLeft } from 'fp-ts/lib/Either'; + +describe('DateFromString', () => { + test('validated and parses a string into a Date', () => { + const date = new Date(1973, 10, 30); + expect(DateFromString.decode(date.toISOString())).toEqual(right(date)); + }); + + test('validated and returns a failure for an actual Date', () => { + const date = new Date(1973, 10, 30); + expect(isLeft(DateFromString.decode(date))).toEqual(true); + }); + + test('validated and returns a failure for an invalid Date string', () => { + expect(isLeft(DateFromString.decode('1234-23-45'))).toEqual(true); + }); + + test('validated and returns a failure for a null value', () => { + expect(isLeft(DateFromString.decode(null))).toEqual(true); + }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/lib/types.ts b/x-pack/legacy/plugins/alerting/server/lib/types.ts new file mode 100644 index 0000000000000..6df593ab17ce8 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/lib/types.ts @@ -0,0 +1,25 @@ +/* + * 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 t from 'io-ts'; +import { either } from 'fp-ts/lib/Either'; +// represents a Date from an ISO string +export const DateFromString = new t.Type( + 'DateFromString', + // detect the type + (value): value is Date => value instanceof Date, + (valueToDecode, context) => + either.chain( + // validate this is a string + t.string.validate(valueToDecode, context), + // decode + value => { + const decoded = new Date(value); + return isNaN(decoded.getTime()) ? t.failure(valueToDecode, context) : t.success(decoded); + } + ), + valueToEncode => valueToEncode.toISOString() +); diff --git a/x-pack/legacy/plugins/alerting/server/plugin.ts b/x-pack/legacy/plugins/alerting/server/plugin.ts index a4de7af376fb0..e3f7656002d18 100644 --- a/x-pack/legacy/plugins/alerting/server/plugin.ts +++ b/x-pack/legacy/plugins/alerting/server/plugin.ts @@ -25,6 +25,7 @@ import { deleteAlertRoute, findAlertRoute, getAlertRoute, + getAlertStateRoute, listAlertTypesRoute, updateAlertRoute, enableAlertRoute, @@ -92,6 +93,7 @@ export class Plugin { core.http.route(extendRouteWithLicenseCheck(deleteAlertRoute, this.licenseState)); core.http.route(extendRouteWithLicenseCheck(findAlertRoute, this.licenseState)); core.http.route(extendRouteWithLicenseCheck(getAlertRoute, this.licenseState)); + core.http.route(extendRouteWithLicenseCheck(getAlertStateRoute, this.licenseState)); core.http.route(extendRouteWithLicenseCheck(listAlertTypesRoute, this.licenseState)); core.http.route(extendRouteWithLicenseCheck(updateAlertRoute, this.licenseState)); core.http.route(extendRouteWithLicenseCheck(enableAlertRoute, this.licenseState)); diff --git a/x-pack/legacy/plugins/alerting/server/routes/get_alert_state.test.ts b/x-pack/legacy/plugins/alerting/server/routes/get_alert_state.test.ts new file mode 100644 index 0000000000000..9e3b3b6579ead --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/routes/get_alert_state.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createMockServer } from './_mock_server'; +import { getAlertStateRoute } from './get_alert_state'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; + +const { server, alertsClient } = createMockServer(); +server.route(getAlertStateRoute); + +const mockedAlertState = { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: new Date(), + }, + }, + }, + second_instance: {}, + }, +}; + +beforeEach(() => jest.resetAllMocks()); + +test('gets alert state', async () => { + const request = { + method: 'GET', + url: '/api/alert/1/state', + }; + + alertsClient.getAlertState.mockResolvedValueOnce(mockedAlertState); + + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + expect(alertsClient.getAlertState).toHaveBeenCalledWith({ id: '1' }); +}); + +test('returns NO-CONTENT when alert exists but has no task state yet', async () => { + const request = { + method: 'GET', + url: '/api/alert/1/state', + }; + + alertsClient.getAlertState.mockResolvedValueOnce(undefined); + + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(204); + expect(alertsClient.getAlertState).toHaveBeenCalledWith({ id: '1' }); +}); + +test('returns NOT-FOUND when alert is not found', async () => { + const request = { + method: 'GET', + url: '/api/alert/1/state', + }; + + alertsClient.getAlertState.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError('alert', '1') + ); + + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(404); + expect(alertsClient.getAlertState).toHaveBeenCalledWith({ id: '1' }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/routes/get_alert_state.ts b/x-pack/legacy/plugins/alerting/server/routes/get_alert_state.ts new file mode 100644 index 0000000000000..12136a975bb19 --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/routes/get_alert_state.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 Joi from 'joi'; +import Hapi from 'hapi'; + +interface GetAlertStateRequest extends Hapi.Request { + params: { + id: string; + }; +} + +export const getAlertStateRoute = { + method: 'GET', + path: '/api/alert/{id}/state', + options: { + tags: ['access:alerting-read'], + validate: { + params: Joi.object() + .keys({ + id: Joi.string().required(), + }) + .required(), + }, + }, + async handler(request: GetAlertStateRequest, h: Hapi.ResponseToolkit) { + const { id } = request.params; + const alertsClient = request.getAlertsClient!(); + const state = await alertsClient.getAlertState({ id }); + return state ? state : h.response().code(204); + }, +}; diff --git a/x-pack/legacy/plugins/alerting/server/routes/index.ts b/x-pack/legacy/plugins/alerting/server/routes/index.ts index 02cba8adc9db2..7ec901ae685c4 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/index.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/index.ts @@ -8,6 +8,7 @@ export { createAlertRoute } from './create'; export { deleteAlertRoute } from './delete'; export { findAlertRoute } from './find'; export { getAlertRoute } from './get'; +export { getAlertStateRoute } from './get_alert_state'; export { listAlertTypesRoute } from './list_alert_types'; export { updateAlertRoute } from './update'; export { enableAlertRoute } from './enable'; diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.test.ts new file mode 100644 index 0000000000000..9cbe91a4dbced --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.test.ts @@ -0,0 +1,229 @@ +/* + * 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 { ConcreteTaskInstance, TaskStatus } from '../../../../../plugins/task_manager/server'; +import { AlertTaskInstance, taskInstanceToAlertTaskInstance } from './alert_task_instance'; +import uuid from 'uuid'; +import { SanitizedAlert } from '../types'; + +const alert: SanitizedAlert = { + id: 'alert-123', + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [], + enabled: true, + name: '', + tags: [], + consumer: '', + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], +}; + +describe('Alert Task Instance', () => { + test(`validates that a TaskInstance has valid Alert Task State`, () => { + const lastScheduledActionsDate = new Date(); + const taskInstance: ConcreteTaskInstance = { + id: uuid.v4(), + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: lastScheduledActionsDate.toISOString(), + }, + }, + }, + second_instance: {}, + }, + }, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + ownerId: null, + }; + + const alertTaskInsatnce: AlertTaskInstance = taskInstanceToAlertTaskInstance(taskInstance); + + expect(alertTaskInsatnce).toEqual({ + ...taskInstance, + state: { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: { + state: {}, + meta: { + lastScheduledActions: { + group: 'first_group', + date: lastScheduledActionsDate, + }, + }, + }, + second_instance: {}, + }, + }, + }); + }); + + test(`throws if state is invalid`, () => { + const taskInstance: ConcreteTaskInstance = { + id: '215ee69b-1df9-428e-ab1a-ccf274f8fa5b', + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: 'invalid', + second_instance: {}, + }, + }, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + ownerId: null, + }; + + expect(() => taskInstanceToAlertTaskInstance(taskInstance)).toThrowErrorMatchingInlineSnapshot( + `"Task \\"215ee69b-1df9-428e-ab1a-ccf274f8fa5b\\" has invalid state at .alertInstances.first_instance"` + ); + }); + + test(`throws with Alert id when alert is present and state is invalid`, () => { + const taskInstance: ConcreteTaskInstance = { + id: '215ee69b-1df9-428e-ab1a-ccf274f8fa5b', + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: { + alertTypeState: { + some: 'value', + }, + alertInstances: { + first_instance: 'invalid', + second_instance: {}, + }, + }, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + ownerId: null, + }; + + expect(() => + taskInstanceToAlertTaskInstance(taskInstance, alert) + ).toThrowErrorMatchingInlineSnapshot( + `"Task \\"215ee69b-1df9-428e-ab1a-ccf274f8fa5b\\" (underlying Alert \\"alert-123\\") has invalid state at .alertInstances.first_instance"` + ); + }); + + test(`allows an initial empty state`, () => { + const taskInstance: ConcreteTaskInstance = { + id: uuid.v4(), + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: {}, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + ownerId: null, + }; + + const alertTaskInsatnce: AlertTaskInstance = taskInstanceToAlertTaskInstance(taskInstance); + + expect(alertTaskInsatnce).toEqual(taskInstance); + }); + + test(`validates that a TaskInstance has valid Params`, () => { + const taskInstance: ConcreteTaskInstance = { + id: uuid.v4(), + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: {}, + taskType: 'alerting:test', + params: { + alertId: '1', + }, + ownerId: null, + }; + + const alertTaskInsatnce: AlertTaskInstance = taskInstanceToAlertTaskInstance( + taskInstance, + alert + ); + + expect(alertTaskInsatnce).toEqual(taskInstance); + }); + + test(`throws if params are invalid`, () => { + const taskInstance: ConcreteTaskInstance = { + id: '215ee69b-1df9-428e-ab1a-ccf274f8fa5b', + attempts: 0, + status: TaskStatus.Running, + version: '123', + runAt: new Date(), + scheduledAt: new Date(), + startedAt: new Date(), + retryAt: new Date(Date.now() + 5 * 60 * 1000), + state: {}, + taskType: 'alerting:test', + params: {}, + ownerId: null, + }; + + expect(() => + taskInstanceToAlertTaskInstance(taskInstance, alert) + ).toThrowErrorMatchingInlineSnapshot( + `"Task \\"215ee69b-1df9-428e-ab1a-ccf274f8fa5b\\" (underlying Alert \\"alert-123\\") has an invalid param at .0.alertId"` + ); + }); +}); diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.ts b/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.ts new file mode 100644 index 0000000000000..33b416fe8e2da --- /dev/null +++ b/x-pack/legacy/plugins/alerting/server/task_runner/alert_task_instance.ts @@ -0,0 +1,66 @@ +/* + * 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 t from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server'; +import { SanitizedAlert } from '../types'; +import { DateFromString } from '../lib/types'; +import { AlertInstance, rawAlertInstance } from '../alert_instance'; + +export interface AlertTaskInstance extends ConcreteTaskInstance { + state: AlertTaskState; +} + +export const alertStateSchema = t.partial({ + alertTypeState: t.record(t.string, t.unknown), + alertInstances: t.record(t.string, rawAlertInstance), + previousStartedAt: t.union([t.null, DateFromString]), +}); +export type AlertInstances = Record; +export type AlertTaskState = t.TypeOf; + +const alertParamsSchema = t.intersection([ + t.type({ + alertId: t.string, + }), + t.partial({ + spaceId: t.string, + }), +]); +export type AlertTaskParams = t.TypeOf; + +const enumerateErrorFields = (e: t.Errors) => + `${e.map(({ context }) => context.map(({ key }) => key).join('.'))}`; + +export function taskInstanceToAlertTaskInstance( + taskInstance: ConcreteTaskInstance, + alert?: SanitizedAlert +): AlertTaskInstance { + return { + ...taskInstance, + params: pipe( + alertParamsSchema.decode(taskInstance.params), + fold((e: t.Errors) => { + throw new Error( + `Task "${taskInstance.id}" ${ + alert ? `(underlying Alert "${alert.id}") ` : '' + }has an invalid param at ${enumerateErrorFields(e)}` + ); + }, t.identity) + ), + state: pipe( + alertStateSchema.decode(taskInstance.state), + fold((e: t.Errors) => { + throw new Error( + `Task "${taskInstance.id}" ${ + alert ? `(underlying Alert "${alert.id}") ` : '' + }has invalid state at ${enumerateErrorFields(e)}` + ); + }, t.identity) + ), + }; +} diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts index 0f643e3d3121c..1466d3ccd274b 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.ts @@ -10,25 +10,32 @@ import { SavedObject } from '../../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance } from '../../../../../plugins/task_manager/server'; import { createExecutionHandler } from './create_execution_handler'; -import { AlertInstance, createAlertInstanceFactory } from '../alert_instance'; +import { AlertInstance, createAlertInstanceFactory, RawAlertInstance } from '../alert_instance'; import { getNextRunAt } from './get_next_run_at'; import { validateAlertTypeParams } from '../lib'; -import { AlertType, RawAlert, IntervalSchedule, Services, State, AlertInfoParams } from '../types'; +import { AlertType, RawAlert, IntervalSchedule, Services, AlertInfoParams } from '../types'; import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type'; - -type AlertInstances = Record; +import { + AlertTaskState, + AlertInstances, + taskInstanceToAlertTaskInstance, +} from './alert_task_instance'; const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' }; interface AlertTaskRunResult { - state: State; + state: AlertTaskState; runAt: Date; } +interface AlertTaskInstance extends ConcreteTaskInstance { + state: AlertTaskState; +} + export class TaskRunner { private context: TaskRunnerContext; private logger: Logger; - private taskInstance: ConcreteTaskInstance; + private taskInstance: AlertTaskInstance; private alertType: AlertType; constructor( @@ -39,7 +46,7 @@ export class TaskRunner { this.context = context; this.logger = context.logger; this.alertType = alertType; - this.taskInstance = taskInstance; + this.taskInstance = taskInstanceToAlertTaskInstance(taskInstance); } async getApiKeyForAlertPermissions(alertId: string, spaceId: string) { @@ -128,7 +135,7 @@ export class TaskRunner { alertInfoParams: AlertInfoParams, executionHandler: ReturnType, spaceId: string - ): Promise { + ): Promise { const { params, throttle, @@ -145,9 +152,9 @@ export class TaskRunner { } = this.taskInstance; const namespace = this.context.spaceIdToNamespace(spaceId); - const alertInstances = mapValues( + const alertInstances = mapValues( alertRawInstances, - alert => new AlertInstance(alert) + rawAlertInstance => new AlertInstance(rawAlertInstance) ); const updatedAlertTypeState = await this.alertType.executor({ @@ -159,7 +166,7 @@ export class TaskRunner { params, state: alertTypeState, startedAt: this.taskInstance.startedAt!, - previousStartedAt: previousStartedAt && new Date(previousStartedAt), + previousStartedAt: previousStartedAt ? new Date(previousStartedAt) : null, spaceId, namespace, name, @@ -171,7 +178,7 @@ export class TaskRunner { // Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object const instancesWithScheduledActions = pick( alertInstances, - alertInstance => alertInstance.hasScheduledActions() + (alertInstance: AlertInstance) => alertInstance.hasScheduledActions() ); if (!muteAll) { @@ -192,8 +199,11 @@ export class TaskRunner { } return { - alertTypeState: updatedAlertTypeState, - alertInstances: instancesWithScheduledActions, + alertTypeState: updatedAlertTypeState || undefined, + alertInstances: mapValues( + instancesWithScheduledActions, + alertInstance => alertInstance.toRaw() + ), }; } @@ -239,7 +249,7 @@ export class TaskRunner { ); return { - state: await promiseResult( + state: await promiseResult( this.validateAndExecuteAlert(services, apiKey, attributes, references) ), runAt: asOk( @@ -264,9 +274,9 @@ export class TaskRunner { const { state, runAt } = await errorAsAlertTaskRunResult(this.loadAlertAttributesAndRun()); return { - state: map( + state: map( state, - (stateUpdates: State) => { + (stateUpdates: AlertTaskState) => { return { ...stateUpdates, previousStartedAt, diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index 9c4a64ff02105..5aef3b1337a88 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -31,7 +31,7 @@ export interface AlertServices extends Services { export interface AlertExecutorOptions { alertId: string; startedAt: Date; - previousStartedAt?: Date; + previousStartedAt: Date | null; services: AlertServices; params: Record; state: State; diff --git a/x-pack/legacy/plugins/graph/index.ts b/x-pack/legacy/plugins/graph/index.ts index f798fa5e9f39d..143d07cfdbd57 100644 --- a/x-pack/legacy/plugins/graph/index.ts +++ b/x-pack/legacy/plugins/graph/index.ts @@ -61,6 +61,7 @@ export const graph: LegacyPluginInitializer = kibana => { navLinkId: 'graph', app: ['graph', 'kibana'], catalogue: ['graph'], + validLicenses: ['platinum', 'enterprise', 'trial'], privileges: { all: { savedObject: { 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/models/file_data_visualizer/file_data_visualizer.js b/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.js deleted file mode 100644 index 28bb7c24cf12e..0000000000000 --- a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.js +++ /dev/null @@ -1,100 +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'; -import fs from 'fs'; -import os from 'os'; -const util = require('util'); -// const readFile = util.promisify(fs.readFile); -const readdir = util.promisify(fs.readdir); -const writeFile = util.promisify(fs.writeFile); - -export function fileDataVisualizerProvider(callWithRequest) { - async function analyzeFile(data, overrides) { - let cached = false; - let results = []; - - try { - results = await callWithRequest('ml.fileStructure', { body: data, ...overrides }); - if (false) { - // disabling caching for now - cached = await cacheData(data); - } - } catch (error) { - const err = error.message !== undefined ? error.message : error; - throw Boom.badRequest(err); - } - - const { hasOverrides, reducedOverrides } = formatOverrides(overrides); - - return { - ...(hasOverrides && { overrides: reducedOverrides }), - cached, - results, - }; - } - - async function cacheData(data) { - const outputPath = `${os.tmpdir()}/kibana-ml`; - const tempFile = 'es-ml-tempFile'; - const tempFilePath = `${outputPath}/${tempFile}`; - - try { - createOutputDir(outputPath); - await deleteOutputFiles(outputPath); - await writeFile(tempFilePath, data); - return true; - } catch (error) { - return false; - } - } - - function createOutputDir(dir) { - if (fs.existsSync(dir) === false) { - fs.mkdirSync(dir); - } - } - - async function deleteOutputFiles(outputPath) { - const files = await readdir(outputPath); - files.forEach(f => { - fs.unlinkSync(`${outputPath}/${f}`); - }); - } - - return { - analyzeFile, - }; -} - -function formatOverrides(overrides) { - let hasOverrides = false; - - const reducedOverrides = Object.keys(overrides).reduce((p, c) => { - if (overrides[c] !== '') { - p[c] = overrides[c]; - hasOverrides = true; - } - return p; - }, {}); - - if (reducedOverrides.column_names !== undefined) { - reducedOverrides.column_names = reducedOverrides.column_names.split(','); - } - - if (reducedOverrides.has_header_row !== undefined) { - reducedOverrides.has_header_row = reducedOverrides.has_header_row === 'true'; - } - - if (reducedOverrides.should_trim_fields !== undefined) { - reducedOverrides.should_trim_fields = reducedOverrides.should_trim_fields === 'true'; - } - - return { - reducedOverrides, - hasOverrides, - }; -} diff --git a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts b/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts new file mode 100644 index 0000000000000..fd5b5221393fc --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts @@ -0,0 +1,103 @@ +/* + * 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'; +import { RequestHandlerContext } from 'kibana/server'; + +export type InputData = any[]; + +export interface InputOverrides { + [key: string]: string; +} + +export type FormattedOverrides = InputOverrides & { + column_names: string[]; + has_header_row: boolean; + should_trim_fields: boolean; +}; + +export interface AnalysisResult { + results: { + charset: string; + has_header_row: boolean; + has_byte_order_marker: boolean; + format: string; + field_stats: { + [fieldName: string]: { + count: number; + cardinality: number; + top_hits: Array<{ count: number; value: any }>; + }; + }; + sample_start: string; + num_messages_analyzed: number; + mappings: { + [fieldName: string]: { + type: string; + }; + }; + quote: string; + delimiter: string; + need_client_timezone: boolean; + num_lines_analyzed: number; + column_names: string[]; + }; + overrides?: FormattedOverrides; +} + +export function fileDataVisualizerProvider(context: RequestHandlerContext) { + async function analyzeFile(data: any, overrides: any): Promise { + let results = []; + + try { + results = await context.ml!.mlClient.callAsCurrentUser('ml.fileStructure', { + body: data, + ...overrides, + }); + } catch (error) { + const err = error.message !== undefined ? error.message : error; + throw Boom.badRequest(err); + } + + const { hasOverrides, reducedOverrides } = formatOverrides(overrides); + + return { + ...(hasOverrides && { overrides: reducedOverrides }), + results, + }; + } + + return { + analyzeFile, + }; +} + +function formatOverrides(overrides: InputOverrides) { + let hasOverrides = false; + + const reducedOverrides: FormattedOverrides = Object.keys(overrides).reduce((acc, overrideKey) => { + const overrideValue: string = overrides[overrideKey]; + if (overrideValue !== '') { + if (overrideKey === 'column_names') { + acc.column_names = overrideValue.split(','); + } else if (overrideKey === 'has_header_row') { + acc.has_header_row = overrideValue === 'true'; + } else if (overrideKey === 'should_trim_fields') { + acc.should_trim_fields = overrideValue === 'true'; + } else { + acc[overrideKey] = overrideValue; + } + + hasOverrides = true; + } + return acc; + }, {} as FormattedOverrides); + + return { + reducedOverrides, + hasOverrides, + }; +} diff --git a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/import_data.js b/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/import_data.ts similarity index 71% rename from x-pack/legacy/plugins/ml/server/models/file_data_visualizer/import_data.js rename to x-pack/legacy/plugins/ml/server/models/file_data_visualizer/import_data.ts index 644a137fbc092..008efb43a6c07 100644 --- a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/import_data.js +++ b/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/import_data.ts @@ -4,10 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext } from 'kibana/server'; import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_datavisualizer'; +import { InputData } from './file_data_visualizer'; -export function importDataProvider(callWithRequest) { - async function importData(id, index, settings, mappings, ingestPipeline, data) { +export interface Settings { + pipeline?: string; + index: string; + body: any[]; + [key: string]: any; +} + +export interface Mappings { + [key: string]: any; +} + +export interface InjectPipeline { + id: string; + pipeline: any; +} + +interface Failure { + item: number; + reason: string; + doc: any; +} + +export function importDataProvider(context: RequestHandlerContext) { + const callAsCurrentUser = context.ml!.mlClient.callAsCurrentUser; + + async function importData( + id: string, + index: string, + settings: Settings, + mappings: Mappings, + ingestPipeline: InjectPipeline, + data: InputData + ) { let createdIndex; let createdPipelineId; const docCount = data.length; @@ -35,7 +68,7 @@ export function importDataProvider(callWithRequest) { createdPipelineId = pipelineId; } - let failures = []; + let failures: Failure[] = []; if (data.length) { const resp = await indexData(index, createdPipelineId, data); if (resp.success === false) { @@ -72,8 +105,8 @@ export function importDataProvider(callWithRequest) { } } - async function createIndex(index, settings, mappings) { - const body = { + async function createIndex(index: string, settings: Settings, mappings: Mappings) { + const body: { mappings: Mappings; settings?: Settings } = { mappings: { _meta: { created_by: INDEX_META_DATA_CREATED_BY, @@ -86,10 +119,10 @@ export function importDataProvider(callWithRequest) { body.settings = settings; } - await callWithRequest('indices.create', { index, body }); + await callAsCurrentUser('indices.create', { index, body }); } - async function indexData(index, pipelineId, data) { + async function indexData(index: string, pipelineId: string, data: InputData) { try { const body = []; for (let i = 0; i < data.length; i++) { @@ -97,12 +130,12 @@ export function importDataProvider(callWithRequest) { body.push(data[i]); } - const settings = { index, body }; + const settings: Settings = { index, body }; if (pipelineId !== undefined) { settings.pipeline = pipelineId; } - const resp = await callWithRequest('bulk', settings); + const resp = await callAsCurrentUser('bulk', settings); if (resp.errors) { throw resp; } else { @@ -113,7 +146,7 @@ export function importDataProvider(callWithRequest) { }; } } catch (error) { - let failures = []; + let failures: Failure[] = []; let ingestError = false; if (error.errors !== undefined && Array.isArray(error.items)) { // an expected error where some or all of the bulk request @@ -134,11 +167,11 @@ export function importDataProvider(callWithRequest) { } } - async function createPipeline(id, pipeline) { - return await callWithRequest('ingest.putPipeline', { id, body: pipeline }); + async function createPipeline(id: string, pipeline: any) { + return await callAsCurrentUser('ingest.putPipeline', { id, body: pipeline }); } - function getFailures(items, data) { + function getFailures(items: any[], data: InputData): Failure[] { const failures = []; for (let i = 0; i < items.length; i++) { const item = items[i]; diff --git a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/index.js b/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/index.js deleted file mode 100644 index 3bda5599e7181..0000000000000 --- a/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/index.js +++ /dev/null @@ -1,8 +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 { fileDataVisualizerProvider } from './file_data_visualizer'; -export { importDataProvider } from './import_data'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/index.ts b/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/index.ts similarity index 53% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/index.ts rename to x-pack/legacy/plugins/ml/server/models/file_data_visualizer/index.ts index d02068a11e8d8..94529dc111696 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/index.ts +++ b/x-pack/legacy/plugins/ml/server/models/file_data_visualizer/index.ts @@ -4,5 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './adapter_types'; -export { elasticsearchMonitorsAdapter } from './elasticsearch_monitors_adapter'; +export { + fileDataVisualizerProvider, + InputOverrides, + InputData, + AnalysisResult, +} from './file_data_visualizer'; + +export { importDataProvider, Settings, InjectPipeline, Mappings } from './import_data'; 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/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json index 574065446827d..1be31e2316228 100644 --- a/x-pack/legacy/plugins/ml/server/routes/apidoc.json +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -3,7 +3,6 @@ "version": "0.1.0", "description": "ML Kibana API", "title": "ML Kibana API", - "url" : "/api/ml/", "order": [ "DataFrameAnalytics", "GetDataFrameAnalytics", @@ -34,6 +33,9 @@ "ForecastAnomalyDetector", "GetOverallBuckets", "GetCategories", + "FileDataVisualizer", + "AnalyzeFile", + "ImportFile" "ResultsService", "GetAnomaliesTableData", "GetCategoryDefinition", 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/ml/server/routes/file_data_visualizer.js b/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.js deleted file mode 100644 index fc6a0ff756928..0000000000000 --- a/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.js +++ /dev/null @@ -1,69 +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 '../client/call_with_request_factory'; -import { wrapError } from '../client/errors'; -import { fileDataVisualizerProvider, importDataProvider } from '../models/file_data_visualizer'; -import { MAX_BYTES } from '../../common/constants/file_datavisualizer'; - -import { incrementFileDataVisualizerIndexCreationCount } from '../lib/ml_telemetry/ml_telemetry'; - -function analyzeFiles(callWithRequest, data, overrides) { - const { analyzeFile } = fileDataVisualizerProvider(callWithRequest); - return analyzeFile(data, overrides); -} - -function importData(callWithRequest, id, index, settings, mappings, ingestPipeline, data) { - const { importData: importDataFunc } = importDataProvider(callWithRequest); - return importDataFunc(id, index, settings, mappings, ingestPipeline, data); -} - -export function fileDataVisualizerRoutes({ - commonRouteConfig, - elasticsearchPlugin, - route, - savedObjects, -}) { - route({ - method: 'POST', - path: '/api/ml/file_data_visualizer/analyze_file', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const data = request.payload; - - return analyzeFiles(callWithRequest, data, request.query).catch(wrapError); - }, - config: { - ...commonRouteConfig, - payload: { maxBytes: MAX_BYTES }, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/file_data_visualizer/import', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { id } = request.query; - const { index, data, settings, mappings, ingestPipeline } = request.payload; - - // `id` being `undefined` tells us that this is a new import due to create a new index. - // follow-up import calls to just add additional data will include the `id` of the created - // index, we'll ignore those and don't increment the counter. - if (id === undefined) { - incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); - } - - return importData(callWithRequest, id, index, settings, mappings, ingestPipeline, data).catch( - wrapError - ); - }, - config: { - ...commonRouteConfig, - payload: { maxBytes: MAX_BYTES }, - }, - }); -} diff --git a/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts new file mode 100644 index 0000000000000..95f2a9fe7298f --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/file_data_visualizer.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandlerContext } from 'kibana/server'; +import { MAX_BYTES } from '../../common/constants/file_datavisualizer'; +import { wrapError } from '../client/error_wrapper'; +import { + InputOverrides, + InputData, + fileDataVisualizerProvider, + importDataProvider, + Settings, + InjectPipeline, + Mappings, +} from '../models/file_data_visualizer'; + +import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { RouteInitialization } from '../new_platform/plugin'; +import { incrementFileDataVisualizerIndexCreationCount } from '../lib/ml_telemetry'; + +function analyzeFiles(context: RequestHandlerContext, data: InputData, overrides: InputOverrides) { + const { analyzeFile } = fileDataVisualizerProvider(context); + return analyzeFile(data, overrides); +} + +function importData( + context: RequestHandlerContext, + id: string, + index: string, + settings: Settings, + mappings: Mappings, + ingestPipeline: InjectPipeline, + data: InputData +) { + const { importData: importDataFunc } = importDataProvider(context); + return importDataFunc(id, index, settings, mappings, ingestPipeline, data); +} + +/** + * Routes for the file data visualizer. + */ +export function fileDataVisualizerRoutes({ + router, + xpackMainPlugin, + savedObjects, + elasticsearchPlugin, +}: RouteInitialization) { + /** + * @apiGroup FileDataVisualizer + * + * @api {post} /api/ml/file_data_visualizer/analyze_file Analyze file data + * @apiName AnalyzeFile + * @apiDescription Performs analysis of the file data. + */ + router.post( + { + path: '/api/ml/file_data_visualizer/analyze_file', + validate: { + body: schema.any(), + query: schema.maybe( + schema.object({ + charset: schema.maybe(schema.string()), + column_names: schema.maybe(schema.string()), + delimiter: schema.maybe(schema.string()), + explain: schema.maybe(schema.string()), + format: schema.maybe(schema.string()), + grok_pattern: schema.maybe(schema.string()), + has_header_row: schema.maybe(schema.string()), + line_merge_size_limit: schema.maybe(schema.string()), + lines_to_sample: schema.maybe(schema.string()), + quote: schema.maybe(schema.string()), + should_trim_fields: schema.maybe(schema.string()), + timeout: schema.maybe(schema.string()), + timestamp_field: schema.maybe(schema.string()), + timestamp_format: schema.maybe(schema.string()), + }) + ), + }, + options: { + body: { + accepts: ['text/*', 'application/json'], + maxBytes: MAX_BYTES, + }, + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const result = await analyzeFiles(context, request.body, request.query); + return response.ok({ body: result }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup FileDataVisualizer + * + * @api {post} /api/ml/file_data_visualizer/import Import file data + * @apiName ImportFile + * @apiDescription Imports file data into elasticsearch index. + */ + router.post( + { + path: '/api/ml/file_data_visualizer/import', + validate: { + query: schema.object({ + id: schema.maybe(schema.string()), + }), + body: schema.object({ + index: schema.maybe(schema.string()), + data: schema.arrayOf(schema.any()), + settings: schema.maybe(schema.any()), + mappings: schema.any(), + ingestPipeline: schema.object({ + id: schema.maybe(schema.string()), + pipeline: schema.maybe(schema.any()), + }), + }), + }, + options: { + body: { + accepts: ['application/json'], + maxBytes: MAX_BYTES, + }, + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { id } = request.query; + const { index, data, settings, mappings, ingestPipeline } = request.body; + + // `id` being `undefined` tells us that this is a new import due to create a new index. + // follow-up import calls to just add additional data will include the `id` of the created + // index, we'll ignore those and don't increment the counter. + if (id === undefined) { + await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects!); + } + + const result = await importData( + context, + id, + index, + settings, + mappings, + ingestPipeline, + data + ); + return response.ok({ body: result }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); +} 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 9294907abcc3f..c0e4873f4a63e 100644 --- a/x-pack/legacy/plugins/monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/index.js @@ -51,6 +51,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/alerts/license_expiration.test.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts index 2fec949f5692e..ec00ece9e6ee2 100644 --- a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts @@ -63,6 +63,7 @@ const alertExecutorOptions: LicenseExpirationAlertExecutorOptions = { spaceId: '', name: '', tags: [], + previousStartedAt: null, createdBy: null, updatedBy: null, }; 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/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 95ca1792a7eb1..7772945d5abad 100644 --- a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -12,7 +12,7 @@ import { toastNotifications } from 'ui/notify'; import chrome from 'ui/chrome'; import { npSetup } from 'ui/new_platform'; -import { IAction, IncompatibleActionError } from '../../../../../../src/plugins/ui_actions/public'; +import { Action, IncompatibleActionError } from '../../../../../../src/plugins/ui_actions/public'; import { ViewMode, @@ -38,7 +38,7 @@ interface ActionContext { embeddable: ISearchEmbeddable; } -class GetCsvReportPanelAction implements IAction { +class GetCsvReportPanelAction implements Action { private isDownloading: boolean; public readonly type = CSV_REPORTING_ACTION; public readonly id = CSV_REPORTING_ACTION; 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 95% 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 e987c8e06d556..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,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const elasticsearchJsPlugin = (Client, config, components) => { - // eslint-disable-line no-unused-vars +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/uptime/server/lib/adapters/monitor_states/index.ts b/x-pack/legacy/plugins/rollup/server/index.ts similarity index 55% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/index.ts rename to x-pack/legacy/plugins/rollup/server/index.ts index a3fc71ad48cb3..6bbd00ac6576e 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/index.ts +++ b/x-pack/legacy/plugins/rollup/server/index.ts @@ -3,6 +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 * from './adapter_types'; -export { elasticsearchMonitorStatesAdapter } from './elasticsearch_monitor_states_adapter'; +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/index.js b/x-pack/legacy/plugins/rollup/server/lib/register_license_checker/index.js deleted file mode 100644 index 7b0f97c38d129..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/register_license_checker/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 { registerLicenseChecker } from './register_license_checker'; 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 daaa211db55cd..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.js +++ /dev/null @@ -1,163 +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) { - // There is an issue opened on ES to handle the following error correctly - // https://github.com/elastic/elasticsearch/issues/39845 - // Until then we'll modify the response here. - if (err.message.includes('Cannot start task for Rollup Job')) { - err.status = 400; - err.statusCode = 400; - err.body.error.status = 400; - err.displayName = 'Bad request'; - } - - 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/39845 - // 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..f9d5944f2e0df --- /dev/null +++ b/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts @@ -0,0 +1,188 @@ +/* + * 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) { + // There is an issue opened on ES to handle the following error correctly + // https://github.com/elastic/elasticsearch/issues/39845 + // Until then we'll modify the response here. + if (err.message.includes('Cannot start task for Rollup Job')) { + err.status = 400; + err.statusCode = 400; + err.body.error.status = 400; + err.displayName = 'Bad request'; + } + + 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/usage/index.js b/x-pack/legacy/plugins/rollup/server/shared_imports.ts similarity index 75% rename from x-pack/legacy/plugins/rollup/server/usage/index.js rename to x-pack/legacy/plugins/rollup/server/shared_imports.ts index 9304b35aeb6c7..941610b97707f 100644 --- a/x-pack/legacy/plugins/rollup/server/usage/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 { registerRollupUsageCollector } from './collector'; +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/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index 3eda945c9224e..dc8c696301611 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -62,13 +62,11 @@ export const LinkToPage = React.memo(({ match }) => ( component={RedirectToDetectionEnginePage} exact path={`${match.url}/:pageName(${SiemPageName.detections})`} - strict /> { +export const decodeRisonUrlState = (value: string | undefined): T | null => { try { - return value ? decode(value) : undefined; + return value ? ((decode(value) as unknown) as T) : null; } catch (error) { if (error instanceof Error && error.message.startsWith('rison decoder error')) { - return {}; + return null; } throw error; } @@ -30,18 +38,16 @@ export const decodeRisonUrlState = (value: string | undefined): RisonValue | any // eslint-disable-next-line @typescript-eslint/no-explicit-any export const encodeRisonUrlState = (state: any) => encode(state); -export const getQueryStringFromLocation = (location: Location) => location.search.substring(1); +export const getQueryStringFromLocation = (search: string) => search.substring(1); export const getParamFromQueryString = (queryString: string, key: string): string | undefined => { const queryParam = QueryString.decode(queryString)[key]; return Array.isArray(queryParam) ? queryParam[0] : queryParam; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const replaceStateKeyInQueryString = ( - stateKey: string, - urlState: UrlState | undefined -) => (queryString: string) => { +export const replaceStateKeyInQueryString = (stateKey: string, urlState: T) => ( + queryString: string +): string => { const previousQueryValues = QueryString.decode(queryString); if (urlState == null || (typeof urlState === 'string' && urlState === '')) { delete previousQueryValues[stateKey]; @@ -60,8 +66,11 @@ export const replaceStateKeyInQueryString = ( }); }; -export const replaceQueryStringInLocation = (location: Location, queryString: string): Location => { - if (queryString === getQueryStringFromLocation(location)) { +export const replaceQueryStringInLocation = ( + location: H.Location, + queryString: string +): H.Location => { + if (queryString === getQueryStringFromLocation(location.search)) { return location; } else { return { @@ -173,3 +182,99 @@ export const makeMapStateToProps = () => { return mapStateToProps; }; + +export const updateTimerangeUrl = ( + timeRange: UrlInputsModel, + isInitializing: boolean +): UrlInputsModel => { + if (timeRange.global.timerange.kind === 'relative') { + timeRange.global.timerange.from = formatDate(timeRange.global.timerange.fromStr); + timeRange.global.timerange.to = formatDate(timeRange.global.timerange.toStr, { roundUp: true }); + } + if (timeRange.timeline.timerange.kind === 'relative' && isInitializing) { + timeRange.timeline.timerange.from = formatDate(timeRange.timeline.timerange.fromStr); + timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr, { + roundUp: true, + }); + } + return timeRange; +}; + +export const updateUrlStateString = ({ + isInitializing, + history, + newUrlStateString, + pathName, + search, + updateTimerange, + urlKey, +}: UpdateUrlStateString): string => { + if (urlKey === CONSTANTS.appQuery) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (queryState != null && queryState.query === '') { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.timerange && updateTimerange) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (queryState != null && queryState.global != null) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: updateTimerangeUrl(queryState, isInitializing), + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.filters) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (isEmpty(queryState)) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.timeline) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (queryState != null && queryState.id === '') { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } + return search; +}; + +export const replaceStateInLocation = ({ + history, + urlStateToReplace, + urlStateKey, + pathName, + search, +}: ReplaceStateInLocation) => { + const newLocation = replaceQueryStringInLocation( + { + hash: '', + pathname: pathName, + search, + state: '', + }, + replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(getQueryStringFromLocation(search)) + ); + if (history) { + history.replace(newLocation); + } + return newLocation.search; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx index 9a9bb409936fc..10aa388449d91 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { HookWrapper } from '../../mock'; import { SiemPageName } from '../../pages/home/types'; import { RouteSpyState } from '../../utils/route/types'; - import { CONSTANTS } from './constants'; import { getMockPropsObj, @@ -22,6 +21,7 @@ import { } from './test_dependencies'; import { UrlStateContainerPropTypes } from './types'; import { useUrlStateHooks } from './use_url_state'; +import { wait } from '../../lib/helpers'; let mockProps: UrlStateContainerPropTypes; @@ -36,6 +36,12 @@ jest.mock('../../utils/route/use_route_spy', () => ({ useRouteSpy: () => [mockRouteSpy], })); +jest.mock('../super_date_picker', () => ({ + formatDate: (date: string) => { + return 11223344556677; + }, +})); + jest.mock('../../lib/kibana', () => ({ useKibana: () => ({ services: { @@ -69,19 +75,19 @@ describe('UrlStateContainer', () => { mount( useUrlStateHooks(args)} />); expect(mockSetRelativeRangeDatePicker.mock.calls[1][0]).toEqual({ - from: 1558591200000, + from: 11223344556677, fromStr: 'now-1d/d', kind: 'relative', - to: 1558677599999, + to: 11223344556677, toStr: 'now-1d/d', id: 'global', }); expect(mockSetRelativeRangeDatePicker.mock.calls[0][0]).toEqual({ - from: 1558732849370, + from: 11223344556677, fromStr: 'now-15m', kind: 'relative', - to: 1558733749370, + to: 11223344556677, toStr: 'now', id: 'timeline', }); @@ -161,4 +167,57 @@ describe('UrlStateContainer', () => { }); }); }); + + describe('After Initialization, keep Relative Date up to date for global only on detections page', () => { + test.each(testCases)( + '%o', + async (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ + page, + examplePath, + namespaceLower, + pageName, + detailName, + }).relativeTimeSearch.undefinedQuery; + const wrapper = mount( + useUrlStateHooks(args)} /> + ); + + wrapper.setProps({ + hookProps: getMockPropsObj({ + page: CONSTANTS.hostsPage, + examplePath: '/hosts', + namespaceLower: 'hosts', + pageName: SiemPageName.hosts, + detailName: undefined, + }).relativeTimeSearch.undefinedQuery, + }); + wrapper.update(); + await wait(); + + if (CONSTANTS.detectionsPage === page) { + expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({ + from: 11223344556677, + fromStr: 'now-1d/d', + kind: 'relative', + to: 11223344556677, + toStr: 'now-1d/d', + id: 'global', + }); + + expect(mockSetRelativeRangeDatePicker.mock.calls[2][0]).toEqual({ + from: 1558732849370, + fromStr: 'now-15m', + kind: 'relative', + to: 1558733749370, + toStr: 'now', + id: 'timeline', + }); + } else { + // There is no change in url state, so that's expected we only have two actions + expect(mockSetRelativeRangeDatePicker.mock.calls.length).toEqual(2); + } + } + ); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx index e182b879651f1..7796fde0fbcb4 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx @@ -20,7 +20,7 @@ import { import { CONSTANTS } from './constants'; import { decodeRisonUrlState } from './helpers'; import { normalizeTimeRange } from './normalize_time_range'; -import { DispatchSetInitialStateFromUrl, SetInitialStateFromUrl } from './types'; +import { DispatchSetInitialStateFromUrl, SetInitialStateFromUrl, Timeline } from './types'; import { queryTimelineById } from '../open_timeline/helpers'; export const dispatchSetInitialStateFromUrl = ( @@ -38,80 +38,11 @@ export const dispatchSetInitialStateFromUrl = ( }: SetInitialStateFromUrl): (() => void) => () => { urlStateToUpdate.forEach(({ urlKey, newUrlStateString }) => { if (urlKey === CONSTANTS.timerange) { - const timerangeStateData: UrlInputsModel = decodeRisonUrlState(newUrlStateString); - - const globalId: InputsModelId = 'global'; - const globalLinkTo: LinkTo = { linkTo: get('global.linkTo', timerangeStateData) }; - const globalType: TimeRangeKinds = get('global.timerange.kind', timerangeStateData); - - const timelineId: InputsModelId = 'timeline'; - const timelineLinkTo: LinkTo = { linkTo: get('timeline.linkTo', timerangeStateData) }; - const timelineType: TimeRangeKinds = get('timeline.timerange.kind', timerangeStateData); - - if (isEmpty(globalLinkTo.linkTo)) { - dispatch(inputsActions.removeGlobalLinkTo()); - } else { - dispatch(inputsActions.addGlobalLinkTo({ linkToId: 'timeline' })); - } - - if (isEmpty(timelineLinkTo.linkTo)) { - dispatch(inputsActions.removeTimelineLinkTo()); - } else { - dispatch(inputsActions.addTimelineLinkTo({ linkToId: 'global' })); - } - - if (timelineType) { - if (timelineType === 'absolute') { - const absoluteRange = normalizeTimeRange( - get('timeline.timerange', timerangeStateData) - ); - dispatch( - inputsActions.setAbsoluteRangeDatePicker({ - ...absoluteRange, - id: timelineId, - }) - ); - } - if (timelineType === 'relative') { - const relativeRange = normalizeTimeRange( - get('timeline.timerange', timerangeStateData) - ); - dispatch( - inputsActions.setRelativeRangeDatePicker({ - ...relativeRange, - id: timelineId, - }) - ); - } - } - - if (globalType) { - if (globalType === 'absolute') { - const absoluteRange = normalizeTimeRange( - get('global.timerange', timerangeStateData) - ); - dispatch( - inputsActions.setAbsoluteRangeDatePicker({ - ...absoluteRange, - id: globalId, - }) - ); - } - if (globalType === 'relative') { - const relativeRange = normalizeTimeRange( - get('global.timerange', timerangeStateData) - ); - dispatch( - inputsActions.setRelativeRangeDatePicker({ - ...relativeRange, - id: globalId, - }) - ); - } - } + updateTimerange(newUrlStateString, dispatch); } + if (urlKey === CONSTANTS.appQuery && indexPattern != null) { - const appQuery: Query = decodeRisonUrlState(newUrlStateString); + const appQuery = decodeRisonUrlState(newUrlStateString); if (appQuery != null) { dispatch( inputsActions.setFilterQuery({ @@ -124,13 +55,13 @@ export const dispatchSetInitialStateFromUrl = ( } if (urlKey === CONSTANTS.filters) { - const filters: esFilters.Filter[] = decodeRisonUrlState(newUrlStateString); + const filters = decodeRisonUrlState(newUrlStateString); filterManager.setFilters(filters || []); } if (urlKey === CONSTANTS.savedQuery) { - const savedQueryId: string = decodeRisonUrlState(newUrlStateString); - if (savedQueryId !== '') { + const savedQueryId = decodeRisonUrlState(newUrlStateString); + if (savedQueryId != null && savedQueryId !== '') { savedQueries.getSavedQuery(savedQueryId).then(savedQueryData => { filterManager.setFilters(savedQueryData.attributes.filters || []); dispatch( @@ -145,7 +76,7 @@ export const dispatchSetInitialStateFromUrl = ( } if (urlKey === CONSTANTS.timeline) { - const timeline = decodeRisonUrlState(newUrlStateString); + const timeline = decodeRisonUrlState(newUrlStateString); if (timeline != null && timeline.id !== '') { queryTimelineById({ apolloClient, @@ -159,3 +90,77 @@ export const dispatchSetInitialStateFromUrl = ( } }); }; + +const updateTimerange = (newUrlStateString: string, dispatch: Dispatch) => { + const timerangeStateData = decodeRisonUrlState(newUrlStateString); + + const globalId: InputsModelId = 'global'; + const globalLinkTo: LinkTo = { linkTo: get('global.linkTo', timerangeStateData) }; + const globalType: TimeRangeKinds = get('global.timerange.kind', timerangeStateData); + + const timelineId: InputsModelId = 'timeline'; + const timelineLinkTo: LinkTo = { linkTo: get('timeline.linkTo', timerangeStateData) }; + const timelineType: TimeRangeKinds = get('timeline.timerange.kind', timerangeStateData); + + if (isEmpty(globalLinkTo.linkTo)) { + dispatch(inputsActions.removeGlobalLinkTo()); + } else { + dispatch(inputsActions.addGlobalLinkTo({ linkToId: 'timeline' })); + } + + if (isEmpty(timelineLinkTo.linkTo)) { + dispatch(inputsActions.removeTimelineLinkTo()); + } else { + dispatch(inputsActions.addTimelineLinkTo({ linkToId: 'global' })); + } + + if (timelineType) { + if (timelineType === 'absolute') { + const absoluteRange = normalizeTimeRange( + get('timeline.timerange', timerangeStateData) + ); + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + ...absoluteRange, + id: timelineId, + }) + ); + } + if (timelineType === 'relative') { + const relativeRange = normalizeTimeRange( + get('timeline.timerange', timerangeStateData) + ); + dispatch( + inputsActions.setRelativeRangeDatePicker({ + ...relativeRange, + id: timelineId, + }) + ); + } + } + + if (globalType) { + if (globalType === 'absolute') { + const absoluteRange = normalizeTimeRange( + get('global.timerange', timerangeStateData) + ); + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + ...absoluteRange, + id: globalId, + }) + ); + } + if (globalType === 'relative') { + const relativeRange = normalizeTimeRange( + get('global.timerange', timerangeStateData) + ); + dispatch( + inputsActions.setRelativeRangeDatePicker({ + ...relativeRange, + id: globalId, + }) + ); + } + } +}; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts b/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts index 4dd92ac58b0a3..dc1b8d428bb20 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts @@ -217,6 +217,18 @@ export const getMockPropsObj = ({ pageName, detailName ), + undefinedLinkQuery: getMockProps( + { + hash: '', + pathname: examplePath, + search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558591200000,fromStr:now-1d%2Fd,kind:relative,to:1558677599999,toStr:now-1d%2Fd)),timeline:(linkTo:!(global),timerange:(from:1558732849370,fromStr:now-15m,kind:relative,to:1558733749370,toStr:now)))`, + state: '', + }, + page, + null, + pageName, + detailName + ), }, absoluteTimeSearch: { undefinedQuery: getMockProps( diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index b5ad26330e671..9ee469f4fd427 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts @@ -5,6 +5,7 @@ */ import ApolloClient from 'apollo-client'; +import * as H from 'history'; import { ActionCreator } from 'typescript-fsa'; import { IIndexPattern, @@ -115,6 +116,7 @@ export type UrlStateContainerPropTypes = RouteSpyState & export interface PreviousLocationUrlState { pathName: string | undefined; + pageName: string | undefined; urlState: UrlState; } @@ -144,3 +146,21 @@ export type DispatchSetInitialStateFromUrl = ({ updateTimelineIsLoading, urlStateToUpdate, }: SetInitialStateFromUrl) => () => void; + +export interface ReplaceStateInLocation { + history?: H.History; + urlStateToReplace: T; + urlStateKey: string; + pathName: string; + search: string; +} + +export interface UpdateUrlStateString { + isInitializing: boolean; + history?: H.History; + newUrlStateString: string; + pathName: string; + search: string; + updateTimerange: boolean; + urlKey: KeyUrlState; +} diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx index 173eabe895c2b..deaf9bbf5011d 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx @@ -4,24 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Location } from 'history'; import { isEqual, difference, isEmpty } from 'lodash/fp'; import { useEffect, useRef, useState } from 'react'; -import { Query, esFilters } from 'src/plugins/data/public'; import { useKibana } from '../../lib/kibana'; -import { UrlInputsModel } from '../../store/inputs/model'; import { useApolloClient } from '../../utils/apollo_context'; - import { CONSTANTS, UrlStateType } from './constants'; import { - replaceQueryStringInLocation, getQueryStringFromLocation, - replaceStateKeyInQueryString, getParamFromQueryString, - decodeRisonUrlState, getUrlType, getTitle, + replaceStateInLocation, + updateUrlStateString, } from './helpers'; import { UrlStateContainerPropTypes, @@ -30,8 +25,8 @@ import { KeyUrlState, ALL_URL_STATE_KEYS, UrlStateToRedux, - Timeline, } from './types'; +import { SiemPageName } from '../../pages/home/types'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef(value); @@ -58,87 +53,90 @@ export const useUrlStateHooks = ({ const [isInitializing, setIsInitializing] = useState(true); const apolloClient = useApolloClient(); const { filterManager, savedQueries } = useKibana().services.data.query; - const prevProps = usePrevious({ pathName, urlState }); - - const replaceStateInLocation = ( - urlStateToReplace: UrlInputsModel | Query | esFilters.Filter[] | Timeline | string, - urlStateKey: string, - latestLocation: Location = { - hash: '', - pathname: pathName, - search, - state: '', - } - ) => { - const newLocation = replaceQueryStringInLocation( - { - hash: '', - pathname: pathName, - search, - state: '', - }, - replaceStateKeyInQueryString( - urlStateKey, - urlStateToReplace - )(getQueryStringFromLocation(latestLocation)) - ); - if (history) { - history.replace(newLocation); - } - return newLocation; - }; + const prevProps = usePrevious({ pathName, pageName, urlState }); - const handleInitialize = (initLocation: Location, type: UrlStateType) => { - let myLocation: Location = initLocation; + const handleInitialize = (type: UrlStateType, needUpdate?: boolean) => { + let mySearch = search; let urlStateToUpdate: UrlStateToRedux[] = []; URL_STATE_KEYS[type].forEach((urlKey: KeyUrlState) => { const newUrlStateString = getParamFromQueryString( - getQueryStringFromLocation(initLocation), + getQueryStringFromLocation(mySearch), urlKey ); if (newUrlStateString) { - const queryState: Query | Timeline | esFilters.Filter[] = decodeRisonUrlState( - newUrlStateString - ); - - if ( - urlKey === CONSTANTS.appQuery && - queryState != null && - (queryState as Query).query === '' - ) { - myLocation = replaceStateInLocation('', urlKey, myLocation); - } else if (urlKey === CONSTANTS.filters && isEmpty(queryState)) { - myLocation = replaceStateInLocation('', urlKey, myLocation); - } else if ( - urlKey === CONSTANTS.timeline && - queryState != null && - (queryState as Timeline).id === '' - ) { - myLocation = replaceStateInLocation('', urlKey, myLocation); - } - if (isInitializing) { - urlStateToUpdate = [...urlStateToUpdate, { urlKey, newUrlStateString }]; + mySearch = updateUrlStateString({ + history, + isInitializing, + newUrlStateString, + pathName, + search: mySearch, + updateTimerange: (needUpdate ?? false) || isInitializing, + urlKey, + }); + if (isInitializing || needUpdate) { + const updatedUrlStateString = + getParamFromQueryString(getQueryStringFromLocation(mySearch), urlKey) ?? + newUrlStateString; + if (isInitializing || !isEqual(updatedUrlStateString, newUrlStateString)) { + urlStateToUpdate = [ + ...urlStateToUpdate, + { + urlKey, + newUrlStateString: updatedUrlStateString, + }, + ]; + } } } else if ( urlKey === CONSTANTS.appQuery && urlState[urlKey] != null && - (urlState[urlKey] as Query).query === '' + urlState[urlKey]?.query === '' ) { - myLocation = replaceStateInLocation('', urlKey, myLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); } else if (urlKey === CONSTANTS.filters && isEmpty(urlState[urlKey])) { - myLocation = replaceStateInLocation('', urlKey, myLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); } else if ( urlKey === CONSTANTS.timeline && urlState[urlKey] != null && - (urlState[urlKey] as Timeline).id === '' + urlState[urlKey].id === '' ) { - myLocation = replaceStateInLocation('', urlKey, myLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); } else { - myLocation = replaceStateInLocation(urlState[urlKey] || '', urlKey, myLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: urlState[urlKey] || '', + urlStateKey: urlKey, + }); } }); difference(ALL_URL_STATE_KEYS, URL_STATE_KEYS[type]).forEach((urlKey: KeyUrlState) => { - myLocation = replaceStateInLocation('', urlKey, myLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); }); setInitialStateFromUrl({ @@ -156,41 +154,58 @@ export const useUrlStateHooks = ({ useEffect(() => { const type: UrlStateType = getUrlType(pageName); - const location: Location = { - hash: '', - pathname: pathName, - search, - state: '', - }; - if (isInitializing && pageName != null && pageName !== '') { - handleInitialize(location, type); + handleInitialize(type); setIsInitializing(false); } else if (!isEqual(urlState, prevProps.urlState) && !isInitializing) { - let newLocation: Location = location; + let mySearch = search; URL_STATE_KEYS[type].forEach((urlKey: KeyUrlState) => { if ( urlKey === CONSTANTS.appQuery && urlState[urlKey] != null && - (urlState[urlKey] as Query).query === '' + urlState[urlKey]?.query === '' ) { - newLocation = replaceStateInLocation('', urlKey, newLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); } else if (urlKey === CONSTANTS.filters && isEmpty(urlState[urlKey])) { - newLocation = replaceStateInLocation('', urlKey, newLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); } else if ( urlKey === CONSTANTS.timeline && urlState[urlKey] != null && - (urlState[urlKey] as Timeline).id === '' + urlState[urlKey].id === '' ) { - newLocation = replaceStateInLocation('', urlKey, newLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: '', + urlStateKey: urlKey, + }); } else { - newLocation = replaceStateInLocation(urlState[urlKey] || '', urlKey, newLocation); + mySearch = replaceStateInLocation({ + history, + pathName, + search: mySearch, + urlStateToReplace: urlState[urlKey] || '', + urlStateKey: urlKey, + }); } }); } else if (pathName !== prevProps.pathName) { - handleInitialize(location, type); + handleInitialize(type, pageName === SiemPageName.detections); } - }); + }, [isInitializing, history, pathName, pageName, prevProps, urlState]); useEffect(() => { document.title = `${getTitle(pageName, detailName, navTabs)} - Kibana`; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index ff6722840fd6b..229593901691b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; @@ -24,6 +24,8 @@ import { } from '../../components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../components/search_bar'; import { WrapperPage } from '../../components/wrapper_page'; +import { SiemNavigation } from '../../components/navigation'; +import { NavTab } from '../../components/navigation/types'; import { State } from '../../store'; import { inputsSelectors } from '../../store/inputs'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; @@ -60,18 +62,22 @@ export interface DispatchProps { type DetectionEnginePageComponentProps = ReduxProps & DispatchProps; -const detectionsTabs = [ - { +const detectionsTabs: Record = { + [DetectionEngineTab.signals]: { id: DetectionEngineTab.signals, name: i18n.SIGNAL, + href: getDetectionEngineTabUrl(DetectionEngineTab.signals), disabled: false, + urlKey: 'detections', }, - { + [DetectionEngineTab.alerts]: { id: DetectionEngineTab.alerts, name: i18n.ALERT, + href: getDetectionEngineTabUrl(DetectionEngineTab.alerts), disabled: false, + urlKey: 'detections', }, -]; +}; const DetectionEnginePageComponent: React.FC = ({ filters, @@ -98,24 +104,6 @@ const DetectionEnginePageComponent: React.FC [setAbsoluteRangeDatePicker] ); - const tabs = useMemo( - () => ( - - {detectionsTabs.map(tab => ( - - {tab.name} - - ))} - - ), - [detectionsTabs, tabName] - ); - const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ signalIndexName, ]); @@ -169,7 +157,7 @@ const DetectionEnginePageComponent: React.FC {({ to, from, deleteQuery, setQuery }) => ( <> - {tabs} + {tabName === DetectionEngineTab.signals && ( <> diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx index e47147db474c9..8be5510cda83a 100644 --- a/x-pack/legacy/plugins/siem/public/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/plugin.tsx @@ -16,7 +16,7 @@ import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { IEmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { Start as NewsfeedStart } from '../../../../../src/plugins/newsfeed/public'; import { Start as InspectorStart } from '../../../../../src/plugins/inspector/public'; -import { IUiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public'; import { initTelemetry } from './lib/telemetry'; import { KibanaServices } from './lib/kibana'; @@ -32,7 +32,7 @@ export interface StartPlugins { embeddable: IEmbeddableStart; inspector: InspectorStart; newsfeed?: NewsfeedStart; - uiActions: IUiActionsStart; + uiActions: UiActionsStart; } export type StartServices = CoreStart & StartPlugins; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_dns_directly_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_dns_directly_to_the_internet.json index 72e2ca1490417..56c11c236eecb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_dns_directly_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_dns_directly_to_the_internet.json @@ -4,10 +4,7 @@ "Exclude DNS servers from this rule as this is expected behavior. Endpoints usually query local DNS servers defined in their DHCP scopes, but this may be overridden if a user configures their endpoint to use a remote DNS server. This is uncommon in managed enterprise networks because it could break intranet name resolution when split horizon DNS is utilized. Some consumer VPN services and browser plug-ins may send DNS traffic to remote Internet destinations. In that case, such devices or networks can be excluded from this rule when this is expected behavior." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -42,5 +39,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ftp_file_transfer_protocol_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ftp_file_transfer_protocol_activity_to_the_internet.json index 54daf8a2091a7..a3a692596090c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ftp_file_transfer_protocol_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ftp_file_transfer_protocol_activity_to_the_internet.json @@ -4,10 +4,7 @@ "FTP servers should be excluded from this rule as this is expected behavior. Some business workflows may use FTP for data exchange. These workflows often have expected characteristics such as users, sources, and destinations. FTP activity involving an unusual source or destination may be more suspicious. FTP activity involving a production server that has no known associated FTP workflow or business requirement is often suspicious." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -53,5 +50,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json index d01006a225886..0b5259d3417f5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_irc_internet_relay_chat_protocol_activity_to_the_internet.json @@ -4,10 +4,7 @@ "IRC activity may be normal behavior for developers and engineers but is unusual for non-engineering end users. IRC activity involving an unusual source or destination may be more suspicious. IRC activity involving a production server is often suspicious. Because these ports are in the ephemeral range, this rule may false under certain conditions, such as when a NAT-ed web server replies to a client which has used a port in the range by coincidence. In this case, these servers can be excluded. Some legacy applications may use these ports, but this is very uncommon and usually only appears in local traffic using private IPs, which does not match this rule's conditions." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -53,5 +50,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_nat_traversal_port_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_nat_traversal_port_activity.json index c66a9e9d77fe4..675fd588a1834 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_nat_traversal_port_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_nat_traversal_port_activity.json @@ -4,10 +4,7 @@ "Some networks may utilize these protocols but usage that is unfamiliar to local network administrators can be unexpected and suspicious. Because this port is in the ephemeral range, this rule may false under certain conditions, such as when an application server with a public IP address replies to a client which has used a UDP port in the range by coincidence. This is uncommon but such servers can be excluded." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_26_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_26_activity.json index 7b26141898532..bc00383f94528 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_26_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_26_activity.json @@ -4,10 +4,7 @@ "Servers that process email traffic may cause false positives and should be excluded from this rule as this is expected behavior." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -57,5 +54,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_8000_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_8000_activity_to_the_internet.json index 7551119cd5a84..f418648bebdb9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_8000_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_port_8000_activity_to_the_internet.json @@ -4,10 +4,7 @@ "Because this port is in the ephemeral range, this rule may false under certain conditions, such as when a NATed web server replies to a client which has used a port in the range by coincidence. In this case, such servers can be excluded. Some applications may use this port but this is very uncommon and usually appears in local traffic using private IPs, which this rule does not match. Some cloud environments, particularly development environments, may use this port when VPNs or direct connects are not in use and cloud instances are accessed across the Internet." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_pptp_point_to_point_tunneling_protocol_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_pptp_point_to_point_tunneling_protocol_activity.json index eeb38f756c67a..2321b813a1552 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_pptp_point_to_point_tunneling_protocol_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_pptp_point_to_point_tunneling_protocol_activity.json @@ -4,10 +4,7 @@ "Some networks may utilize PPTP protocols but this is uncommon as more modern VPN technologies are available. Usage that is unfamiliar to local network administrators can be unexpected and suspicious. Torrenting applications may use this port. Because this port is in the ephemeral range, this rule may false under certain conditions, such as when an application server replies to a client that used this port by coincidence. This is uncommon but such servers can be excluded." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_proxy_port_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_proxy_port_activity_to_the_internet.json index 981a7bdffcfed..58bba5b3fa712 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_proxy_port_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_proxy_port_activity_to_the_internet.json @@ -4,10 +4,7 @@ "Some proxied applications may use these ports but this usually occurs in local traffic using private IPs\n which this rule does not match. Proxies are widely used as a security technology but in enterprise environments\n this is usually local traffic which this rule does not match. Internet proxy services using these ports can be\n white-listed if desired. Some screen recording applications may use these ports. Proxy port activity involving\n an unusual source or destination may be more suspicious. Some cloud environments may use this port when VPNs or\n direct connects are not in use and cloud instances are accessed across the Internet. Because these ports are in\n the ephemeral range, this rule may false under certain conditions such as when a NATed web server replies to a\n client which has used a port in the range by coincidence. In this case, such servers can be excluded if desired." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json index 504df93f2f8ed..03e507753cd22 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_from_the_internet.json @@ -4,10 +4,7 @@ " Some network security policies allow RDP directly from the Internet but usage that is unfamiliar to\n server or network owners can be unexpected and suspicious. RDP services may be exposed directly to the\n Internet in some networks such as cloud environments. In such cases, only RDP gateways, bastions or jump\n servers may be expected expose RDP directly to the Internet and can be exempted from this rule. RDP may\n be required by some work-flows such as remote access and support for specialized software products and\n servers. Such work-flows are usually known and not unexpected." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -68,5 +65,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_to_the_internet.json index 2d9fa6ba06dfd..af2279b2d9008 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rdp_remote_desktop_protocol_to_the_internet.json @@ -4,10 +4,7 @@ "RDP connections may be made directly to Internet destinations in order to access\n Windows cloud server instances but such connections are usually made only by engineers.\n In such cases, only RDP gateways, bastions or jump servers may be expected Internet\n destinations and can be exempted from this rule. RDP may be required by some work-flows\n such as remote access and support for specialized software products and servers. Such\n work-flows are usually known and not unexpected. Usage that is unfamiliar to server or\n network owners can be unexpected and suspicious." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -53,5 +50,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_from_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_from_the_internet.json index d50c79db81ba5..4539d639a593a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_from_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_from_the_internet.json @@ -1,10 +1,7 @@ { "description": "This rule detects network events that may indicate the use of RPC traffic\nfrom the Internet. RPC is commonly used by system administrators to remotely\ncontrol a system for maintenance or to use shared resources. It should almost\nnever be directly exposed to the Internet, as it is frequently targeted and\nexploited by threat actors as an initial access or back-door vector.\n", "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -35,5 +32,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_to_the_internet.json index ade7b661a7909..dd1b57572bcb3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_rpc_remote_procedure_call_to_the_internet.json @@ -1,10 +1,7 @@ { "description": "This rule detects network events that may indicate the use of RPC traffic\nto the Internet. RPC is commonly used by system administrators to remotely\ncontrol a system for maintenance or to use shared resources. It should almost\nnever be directly exposed to the Internet, as it is frequently targeted and\nexploited by threat actors as an initial access or back-door vector.\n", "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -35,5 +32,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smb_windows_file_sharing_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smb_windows_file_sharing_activity_to_the_internet.json index 62c2fafb7404f..8b97df2182992 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smb_windows_file_sharing_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smb_windows_file_sharing_activity_to_the_internet.json @@ -1,10 +1,7 @@ { "description": "This rule detects network events that may indicate the use of Windows\nfile sharing (also called SMB or CIFS) traffic to the Internet. SMB is commonly\nused within networks to share files, printers, and other system resources amongst\ntrusted systems. It should almost never be directly exposed to the Internet, as\nit is frequently targeted and exploited by threat actors as an initial access\nor back-door vector or for data exfiltration.\n", "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -50,5 +47,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smtp_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smtp_to_the_internet.json index 02fca5603910e..c6aa5eef372f4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smtp_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_smtp_to_the_internet.json @@ -4,10 +4,7 @@ "NATed servers that process email traffic may false and should be excluded from this rule as this is expected behavior for them. Consumer and personal devices may send email traffic to remote Internet destinations. In this case, such devices or networks can be excluded from this rule if this is expected behavior." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -53,5 +50,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_sql_server_port_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_sql_server_port_activity_to_the_internet.json index 67e6a08ddf791..f11d9705bbda4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_sql_server_port_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_sql_server_port_activity_to_the_internet.json @@ -4,10 +4,7 @@ "Because these ports are in the ephemeral range, this rule may false under certain conditions\n such as when a NATed web server replies to a client which has used a port in the range by\n coincidence. In this case, such servers can be excluded if desired. Some cloud environments may\n use this port when VPNs or direct connects are not in use and database instances are accessed\n directly across the Internet." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_from_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_from_the_internet.json index 052600a0db68a..a95447fc088df 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_from_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_from_the_internet.json @@ -4,10 +4,7 @@ "Some network security policies allow SSH directly from the Internet but usage that is\n unfamiliar to server or network owners can be unexpected and suspicious. SSH services may\n be exposed directly to the Internet in some networks such as cloud environments. In such\n cases, only SSH gateways, bastions or jump servers may be expected expose SSH directly to\n the Internet and can be exempted from this rule. SSH may be required by some work-flows\n such as remote access and support for specialized software products and servers. Such\n work-flows are usually known and not unexpected." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -68,5 +65,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_to_the_internet.json index e3c3135c9240d..b17d35f96324d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_ssh_secure_shell_to_the_internet.json @@ -4,10 +4,7 @@ "SSH connections may be made directly to Internet destinations in order to access Linux\n cloud server instances but such connections are usually made only by engineers. In such cases,\n only SSH gateways, bastions or jump servers may be expected Internet destinations and can be\n exempted from this rule. SSH may be required by some work-flows such as remote access and support\n for specialized software products and servers. Such work-flows are usually known and not unexpected.\n Usage that is unfamiliar to server or network owners can be unexpected and suspicious." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_telnet_port_activity.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_telnet_port_activity.json index c05791c8a107d..99813595013cf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_telnet_port_activity.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_telnet_port_activity.json @@ -4,10 +4,7 @@ "IoT (Internet of Things) devices and networks may use telnet and can be excluded if\n desired. Some business work-flows may use Telnet for administration of older devices. These\n often have a predictable behavior. Telnet activity involving an unusual source or destination\n may be more suspicious. Telnet activity involving a production server that has no known\n associated Telnet work-flow or business requirement is often suspicious." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -68,5 +65,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_tor_activity_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_tor_activity_to_the_internet.json index 64397716eded2..47960f879dfb6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_tor_activity_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_tor_activity_to_the_internet.json @@ -4,10 +4,7 @@ "Tor client activity is uncommon in managed enterprise networks but may be common\n in unmanaged or public networks where few security policies apply. Because these ports\n are in the ephemeral range, this rule may false under certain conditions such as when a\n NATed web server replies to a client which has used one of these ports by coincidence.\n In this case, such servers can be excluded if desired." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -53,5 +50,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_from_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_from_the_internet.json index dc4fbb281f762..d9195a2d2e98c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_from_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_from_the_internet.json @@ -4,10 +4,7 @@ "VNC connections may be received directly to Linux cloud server instances but\n such connections are usually made only by engineers. VNC is less common than SSH\n or RDP but may be required by some work-flows such as remote access and support\n for specialized software products or servers. Such work-flows are usually known\n and not unexpected. Usage that is unfamiliar to server or network owners can be\n unexpected and suspicious." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -53,5 +50,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_to_the_internet.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_to_the_internet.json index 7da5db39d9bfe..57131e28ee9a7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_to_the_internet.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/network_vnc_virtual_network_computing_to_the_internet.json @@ -4,10 +4,7 @@ "VNC connections may be made directly to Linux cloud server instances but such\n connections are usually made only by engineers. VNC is less common than SSH or RDP\n but may be required by some work flows such as remote access and support for\n specialized software products or servers. Such work-flows are usually known and not\n unexpected. Usage that is unfamiliar to server or network owners can be unexpected\n and suspicious." ], "index": [ - "auditbeat-*", - "filebeat-*", - "packetbeat-*", - "winlogbeat-*" + "filebeat-*" ], "language": "kuery", "max_signals": 100, @@ -38,5 +35,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/legacy/plugins/task_manager/server/legacy.ts b/x-pack/legacy/plugins/task_manager/server/legacy.ts index f5e81bfd90169..cd2047b757e61 100644 --- a/x-pack/legacy/plugins/task_manager/server/legacy.ts +++ b/x-pack/legacy/plugins/task_manager/server/legacy.ts @@ -47,6 +47,7 @@ export function createLegacyApi(legacyTaskManager: Promise): Legacy legacyTaskManager.then((tm: TaskManager) => tm.registerTaskDefinitions(taskDefinitions)); }, fetch: (opts: SearchOpts) => legacyTaskManager.then((tm: TaskManager) => tm.fetch(opts)), + get: (id: string) => legacyTaskManager.then((tm: TaskManager) => tm.get(id)), remove: (id: string) => legacyTaskManager.then((tm: TaskManager) => tm.remove(id)), schedule: (taskInstance: TaskInstanceWithDeprecatedFields, options?: any) => legacyTaskManager.then((tm: TaskManager) => tm.schedule(taskInstance, options)), diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap index 9699a1842ccf1..280e05184837f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap @@ -7,6 +7,11 @@ exports[`DonutChart component passes correct props without errors for valid prop >
`; + +exports[`DonutChart component renders a green check when all monitors are up 1`] = ` +.c2.c2 { + margin-left: 0px; + margin-right: 0px; +} + +.c3 { + text-align: right; +} + +.c1 { + max-width: 260px; + min-width: 100px; +} + +.c0 { + height: 42px; + width: 42px; + color: #017d73; + top: 51px; + left: 51px; + position: absolute; +} + +@media (max-width:767px) { + .c1 { + min-width: 0px; + max-width: 100px; + } +} + +
+
+ + +
+
+
+
+ +
+
+
+ +
+
+
+
+ + + Down + + + 0 + +
+
+
+ +
+
+
+ +
+
+
+
+ + + Up + + + 95 + +
+
+
+
+`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart.test.tsx index e98efc893b53b..31939c3dfdb56 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/donut_chart.test.tsx @@ -32,4 +32,20 @@ describe('DonutChart component', () => { const wrapper = renderWithIntl(); expect(wrapper).toMatchSnapshot(); }); + + it('renders a green check when all monitors are up', () => { + const props = { + down: 0, + up: 95, + height: 125, + width: 125, + }; + + const wrapper = renderWithIntl(); + expect(wrapper).toMatchSnapshot(); + + const greenCheck = wrapper.find('.greenCheckIcon'); + + expect(greenCheck).toHaveLength(1); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx index 09e28efb4e50b..a4bbe486a9317 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/donut_chart.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import React, { useContext, useEffect, useRef } from 'react'; import * as d3 from 'd3'; import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; import { DonutChartLegend } from './donut_chart_legend'; import { UptimeThemeContext } from '../../../contexts'; @@ -18,6 +19,15 @@ interface DonutChartProps { width: number; } +export const GreenCheckIcon = styled(EuiIcon)` + height: 42px; + width: 42px; + color: #017d73; + top: 51px; + left: 51px; + position: absolute; +`; + export const DonutChart = ({ height, down, up, width }: DonutChartProps) => { const chartElement = useRef(null); @@ -77,7 +87,7 @@ export const DonutChart = ({ height, down, up, width }: DonutChartProps) => { return ( - + { width={width} height={height} /> + {/* When all monitors are up we show green check icon in middle of donut to indicate, all is well */} + {down === 0 && } diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitor_states/resolvers.ts b/x-pack/legacy/plugins/uptime/server/graphql/monitor_states/resolvers.ts index 8ddb07b3093d0..45b073086d212 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitor_states/resolvers.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/monitor_states/resolvers.ts @@ -50,8 +50,8 @@ export const createMonitorStatesResolvers: CreateUMGraphQLResolvers = ( totalSummaryCount, { summaries, nextPagePagination, prevPagePagination }, ] = await Promise.all([ - libs.pings.getDocCount({ callES: APICaller }), - libs.monitorStates.getMonitorStates({ + libs.requests.getDocCount({ callES: APICaller }), + libs.requests.getMonitorStates({ callES: APICaller, dateRangeStart, dateRangeEnd, @@ -71,7 +71,7 @@ export const createMonitorStatesResolvers: CreateUMGraphQLResolvers = ( }; }, async getStatesIndexStatus(_resolver, {}, { APICaller }): Promise { - return await libs.monitorStates.statesIndexExists({ callES: APICaller }); + return await libs.requests.getStatesIndexStatus({ callES: APICaller }); }, }, }; diff --git a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts b/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts index cc5744eac6ea1..19f23fa1bb934 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/monitors/resolvers.ts @@ -29,7 +29,7 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = ( { monitorId, dateRangeStart, dateRangeEnd, location }, { APICaller } ): Promise { - return libs.monitors.getMonitorChartsData({ + return await libs.requests.getMonitorCharts({ callES: APICaller, monitorId, dateRangeStart, diff --git a/x-pack/legacy/plugins/uptime/server/graphql/pings/resolvers.ts b/x-pack/legacy/plugins/uptime/server/graphql/pings/resolvers.ts index 373e1467433a2..dea7469ab6217 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/pings/resolvers.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/pings/resolvers.ts @@ -38,7 +38,7 @@ export const createPingsResolvers: CreateUMGraphQLResolvers = ( { monitorId, sort, size, status, dateRangeStart, dateRangeEnd, location }, { APICaller } ): Promise { - return await libs.pings.getAll({ + return await libs.requests.getPings({ callES: APICaller, dateRangeStart, dateRangeEnd, @@ -50,7 +50,7 @@ export const createPingsResolvers: CreateUMGraphQLResolvers = ( }); }, async getDocCount(_resolver, _args, { APICaller }): Promise { - return libs.pings.getDocCount({ callES: APICaller }); + return libs.requests.getDocCount({ callES: APICaller }); }, }, }); diff --git a/x-pack/legacy/plugins/uptime/server/graphql/types.ts b/x-pack/legacy/plugins/uptime/server/graphql/types.ts index 529ab41a62b3b..8508066a71f98 100644 --- a/x-pack/legacy/plugins/uptime/server/graphql/types.ts +++ b/x-pack/legacy/plugins/uptime/server/graphql/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext, CallAPIOptions } from 'kibana/server'; +import { RequestHandlerContext, CallAPIOptions } from 'src/core/server'; import { UMServerLibs } from '../lib/lib'; export type UMContext = RequestHandlerContext & { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/index.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/index.ts index fbef70f106dd8..9b3b11712b99b 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/index.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/index.ts @@ -5,8 +5,4 @@ */ export * from './framework'; -export * from './monitor_states'; -export * from './monitors'; -export * from './pings'; -export * from './stub_index_pattern'; export * from './telemetry'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts deleted file mode 100644 index 4104a9287a28d..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/adapter_types.ts +++ /dev/null @@ -1,64 +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 { - MonitorSummary, - CursorDirection, - SortOrder, - StatesIndexStatus, -} from '../../../../common/graphql/types'; -import { UMElasticsearchQueryFn } from '../framework'; -import { Snapshot } from '../../../../common/runtime_types'; - -export interface MonitorStatesParams { - dateRangeStart: string; - dateRangeEnd: string; - pagination?: CursorPagination; - filters?: string | null; - statusFilter?: string; -} - -export interface GetSnapshotCountParams { - dateRangeStart: string; - dateRangeEnd: string; - filters?: string | null; - statusFilter?: string; -} - -export interface UMMonitorStatesAdapter { - getMonitorStates: UMElasticsearchQueryFn; - getSnapshotCount: UMElasticsearchQueryFn; - statesIndexExists: UMElasticsearchQueryFn<{}, StatesIndexStatus>; -} - -export interface CursorPagination { - cursorKey?: any; - cursorDirection: CursorDirection; - sortOrder: SortOrder; -} - -export interface GetMonitorStatesResult { - summaries: MonitorSummary[]; - nextPagePagination: string | null; - prevPagePagination: string | null; -} - -export interface LegacyMonitorStatesQueryResult { - result: any; - statusFilter?: any; - searchAfter: any; -} - -export interface MonitorStatesCheckGroupsResult { - checkGroups: string[]; - searchAfter: any; -} - -export interface EnrichMonitorStatesResult { - monitors: any[]; - nextPagePagination: any | null; - prevPagePagination: any | null; -} diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts deleted file mode 100644 index 7ed973e6f057d..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/elasticsearch_monitor_states_adapter.ts +++ /dev/null @@ -1,154 +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 { UMMonitorStatesAdapter } from './adapter_types'; -import { INDEX_NAMES, CONTEXT_DEFAULTS } from '../../../../common/constants'; -import { fetchPage } from './search'; -import { MonitorGroupIterator } from './search/monitor_group_iterator'; -import { Snapshot } from '../../../../common/runtime_types'; -import { QueryContext } from './search/query_context'; - -export const elasticsearchMonitorStatesAdapter: UMMonitorStatesAdapter = { - // Gets a page of monitor states. - getMonitorStates: async ({ - callES, - dateRangeStart, - dateRangeEnd, - pagination, - filters, - statusFilter, - }) => { - pagination = pagination || CONTEXT_DEFAULTS.CURSOR_PAGINATION; - statusFilter = statusFilter === null ? undefined : statusFilter; - const size = 10; - - const queryContext = new QueryContext( - callES, - dateRangeStart, - dateRangeEnd, - pagination, - filters && filters !== '' ? JSON.parse(filters) : null, - size, - statusFilter - ); - - const page = await fetchPage(queryContext); - - return { - summaries: page.items, - nextPagePagination: jsonifyPagination(page.nextPagePagination), - prevPagePagination: jsonifyPagination(page.prevPagePagination), - }; - }, - - getSnapshotCount: async ({ - callES, - dateRangeStart, - dateRangeEnd, - filters, - statusFilter, - }): Promise => { - if (!(statusFilter === 'up' || statusFilter === 'down' || statusFilter === undefined)) { - throw new Error(`Invalid status filter value '${statusFilter}'`); - } - - const context = new QueryContext( - callES, - dateRangeStart, - dateRangeEnd, - CONTEXT_DEFAULTS.CURSOR_PAGINATION, - filters && filters !== '' ? JSON.parse(filters) : null, - Infinity, - statusFilter - ); - - // Calculate the total, up, and down counts. - const counts = await fastStatusCount(context); - - // Check if the last count was accurate, if not, we need to perform a slower count with the - // MonitorGroupsIterator. - if (!(await context.hasTimespan())) { - // Figure out whether 'up' or 'down' is more common. It's faster to count the lower cardinality - // one then use subtraction to figure out its opposite. - const [leastCommonStatus, mostCommonStatus]: Array<'up' | 'down'> = - counts.up > counts.down ? ['down', 'up'] : ['up', 'down']; - counts[leastCommonStatus] = await slowStatusCount(context, leastCommonStatus); - counts[mostCommonStatus] = counts.total - counts[leastCommonStatus]; - } - - return { - total: statusFilter ? counts[statusFilter] : counts.total, - up: statusFilter === 'down' ? 0 : counts.up, - down: statusFilter === 'up' ? 0 : counts.down, - }; - }, - - statesIndexExists: async ({ callES }) => { - // TODO: adapt this to the states index in future release - const { - _shards: { total }, - count, - } = await callES('count', { index: INDEX_NAMES.HEARTBEAT }); - return { - indexExists: total > 0, - docCount: { - count, - }, - }; - }, -}; - -// To simplify the handling of the group of pagination vars they're passed back to the client as a string -const jsonifyPagination = (p: any): string | null => { - if (!p) { - return null; - } - - return JSON.stringify(p); -}; - -const fastStatusCount = async (context: QueryContext): Promise => { - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - size: 0, - query: { bool: { filter: await context.dateAndCustomFilters() } }, - aggs: { - unique: { - // We set the precision threshold to 40k which is the max precision supported by cardinality - cardinality: { field: 'monitor.id', precision_threshold: 40000 }, - }, - down: { - filter: { range: { 'summary.down': { gt: 0 } } }, - aggs: { - unique: { cardinality: { field: 'monitor.id', precision_threshold: 40000 } }, - }, - }, - }, - }, - }; - - const statistics = await context.search(params); - const total = statistics.aggregations.unique.value; - const down = statistics.aggregations.down.unique.value; - - return { - total, - down, - up: total - down, - }; -}; - -const slowStatusCount = async (context: QueryContext, status: string): Promise => { - const downContext = context.clone(); - downContext.statusFilter = status; - const iterator = new MonitorGroupIterator(downContext); - let count = 0; - while (await iterator.next()) { - count++; - } - return count; -}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts deleted file mode 100644 index 8523d9c75f51f..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts +++ /dev/null @@ -1,70 +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 { MonitorChart } from '../../../../common/graphql/types'; -import { UMElasticsearchQueryFn } from '../framework'; -import { - MonitorDetails, - MonitorLocations, - OverviewFilters, -} from '../../../../common/runtime_types'; - -export interface GetMonitorChartsDataParams { - /** @member monitorId ID value for the selected monitor */ - monitorId: string; - /** @member dateRangeStart timestamp bounds */ - dateRangeStart: string; - /** @member dateRangeEnd timestamp bounds */ - dateRangeEnd: string; - /** @member location optional location value for use in filtering*/ - location?: string | null; -} - -export interface GetFilterBarParams { - /** @param dateRangeStart timestamp bounds */ - dateRangeStart: string; - /** @member dateRangeEnd timestamp bounds */ - dateRangeEnd: string; - /** @member search this value should correspond to Elasticsearch DSL - * generated from KQL text the user provided. - */ - search?: Record; - filterOptions: Record; -} - -export interface GetMonitorDetailsParams { - monitorId: string; - dateStart: string; - dateEnd: string; -} - -/** - * Fetch data for the monitor page title. - */ -export interface GetMonitorLocationsParams { - /** - * @member monitorId the ID to query - */ - monitorId: string; - dateStart: string; - dateEnd: string; -} - -export interface UMMonitorsAdapter { - /** - * Fetches data used to populate monitor charts - */ - getMonitorChartsData: UMElasticsearchQueryFn; - - /** - * Fetch options for the filter bar. - */ - getFilterBar: UMElasticsearchQueryFn; - - getMonitorDetails: UMElasticsearchQueryFn; - - getMonitorLocations: UMElasticsearchQueryFn; -} diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts deleted file mode 100644 index e433931f03c8e..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts +++ /dev/null @@ -1,385 +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 { get } from 'lodash'; -import { INDEX_NAMES, UNNAMED_LOCATION } from '../../../../common/constants'; -import { MonitorChart, LocationDurationLine } from '../../../../common/graphql/types'; -import { getHistogramIntervalFormatted } from '../../helper'; -import { MonitorError, MonitorLocation } from '../../../../common/runtime_types'; -import { UMMonitorsAdapter } from './adapter_types'; -import { generateFilterAggs } from './generate_filter_aggs'; -import { OverviewFilters } from '../../../../common/runtime_types'; - -export const combineRangeWithFilters = ( - dateRangeStart: string, - dateRangeEnd: string, - filters?: Record -) => { - const range = { - range: { - '@timestamp': { - gte: dateRangeStart, - lte: dateRangeEnd, - }, - }, - }; - if (!filters) return range; - const clientFiltersList = Array.isArray(filters?.bool?.filter ?? {}) - ? // i.e. {"bool":{"filter":{ ...some nested filter objects }}} - filters.bool.filter - : // i.e. {"bool":{"filter":[ ...some listed filter objects ]}} - Object.keys(filters?.bool?.filter ?? {}).map(key => ({ - ...filters?.bool?.filter?.[key], - })); - filters.bool.filter = [...clientFiltersList, range]; - return filters; -}; - -type SupportedFields = 'locations' | 'ports' | 'schemes' | 'tags'; - -export const extractFilterAggsResults = ( - responseAggregations: Record, - keys: SupportedFields[] -): OverviewFilters => { - const values: OverviewFilters = { - locations: [], - ports: [], - schemes: [], - tags: [], - }; - keys.forEach(key => { - const buckets = responseAggregations[key]?.term?.buckets ?? []; - values[key] = buckets.map((item: { key: string | number }) => item.key); - }); - return values; -}; - -const formatStatusBuckets = (time: any, buckets: any, docCount: any) => { - let up = null; - let down = null; - - buckets.forEach((bucket: any) => { - if (bucket.key === 'up') { - up = bucket.doc_count; - } else if (bucket.key === 'down') { - down = bucket.doc_count; - } - }); - - return { - x: time, - up, - down, - total: docCount, - }; -}; - -export const elasticsearchMonitorsAdapter: UMMonitorsAdapter = { - getMonitorChartsData: async ({ callES, dateRangeStart, dateRangeEnd, monitorId, location }) => { - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - query: { - bool: { - filter: [ - { range: { '@timestamp': { gte: dateRangeStart, lte: dateRangeEnd } } }, - { term: { 'monitor.id': monitorId } }, - { term: { 'monitor.status': 'up' } }, - // if location is truthy, add it as a filter. otherwise add nothing - ...(!!location ? [{ term: { 'observer.geo.name': location } }] : []), - ], - }, - }, - size: 0, - aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: getHistogramIntervalFormatted(dateRangeStart, dateRangeEnd), - min_doc_count: 0, - }, - aggs: { - location: { - terms: { - field: 'observer.geo.name', - missing: 'N/A', - }, - aggs: { - status: { terms: { field: 'monitor.status', size: 2, shard_size: 2 } }, - duration: { stats: { field: 'monitor.duration.us' } }, - }, - }, - }, - }, - }, - }, - }; - - const result = await callES('search', params); - - const dateHistogramBuckets = get(result, 'aggregations.timeseries.buckets', []); - /** - * The code below is responsible for formatting the aggregation data we fetched above in a way - * that the chart components used by the client understands. - * There are five required values. Two are lists of points that conform to a simple (x,y) structure. - * - * The third list is for an area chart expressing a range, and it requires an (x,y,y0) structure, - * where y0 is the min value for the point and y is the max. - * - * Additionally, we supply the maximum value for duration and status, so the corresponding charts know - * what the domain size should be. - */ - const monitorChartsData: MonitorChart = { - locationDurationLines: [], - status: [], - durationMaxValue: 0, - statusMaxCount: 0, - }; - - /** - * The following section of code enables us to provide buckets per location - * that have a `null` value if there is no data at the given timestamp. - * - * We maintain two `Set`s. One is per bucket, the other is persisted for the - * entire collection. At the end of a bucket's evaluation, if there was no object - * parsed for a given location line that was already started, we insert an element - * to the given line with a null value. Without this, our charts on the client will - * display a continuous line for each of the points they are provided. - */ - - // a set of all the locations found for this result - const resultLocations = new Set(); - const linesByLocation: { [key: string]: LocationDurationLine } = {}; - dateHistogramBuckets.forEach(dateHistogramBucket => { - const x = get(dateHistogramBucket, 'key'); - const docCount = get(dateHistogramBucket, 'doc_count', 0); - // a set of all the locations for the current bucket - const bucketLocations = new Set(); - - dateHistogramBucket.location.buckets.forEach( - (locationBucket: { key: string; duration: { avg: number } }) => { - const locationName = locationBucket.key; - // store the location name in each set - bucketLocations.add(locationName); - resultLocations.add(locationName); - - // create a new line for this location if it doesn't exist - let currentLine: LocationDurationLine = get(linesByLocation, locationName); - if (!currentLine) { - currentLine = { name: locationName, line: [] }; - linesByLocation[locationName] = currentLine; - monitorChartsData.locationDurationLines.push(currentLine); - } - // add the entry for the current location's duration average - currentLine.line.push({ x, y: get(locationBucket, 'duration.avg', null) }); - } - ); - - // if there are more lines in the result than are represented in the current bucket, - // we must add null entries - if (dateHistogramBucket.location.buckets.length < resultLocations.size) { - resultLocations.forEach(resultLocation => { - // the current bucket has a value for this location, do nothing - if (location && location !== resultLocation) return; - // the current bucket had no value for this location, insert a null value - if (!bucketLocations.has(resultLocation)) { - const locationLine = monitorChartsData.locationDurationLines.find( - ({ name }) => name === resultLocation - ); - // in practice, there should always be a line present, but `find` can return `undefined` - if (locationLine) { - // this will create a gap in the line like we desire - locationLine.line.push({ x, y: null }); - } - } - }); - } - - monitorChartsData.status.push( - formatStatusBuckets(x, get(dateHistogramBucket, 'status.buckets', []), docCount) - ); - }); - - return monitorChartsData; - }, - - getFilterBar: async ({ callES, dateRangeStart, dateRangeEnd, search, filterOptions }) => { - const aggs = generateFilterAggs( - [ - { aggName: 'locations', filterName: 'locations', field: 'observer.geo.name' }, - { aggName: 'ports', filterName: 'ports', field: 'url.port' }, - { aggName: 'schemes', filterName: 'schemes', field: 'monitor.type' }, - { aggName: 'tags', filterName: 'tags', field: 'tags' }, - ], - filterOptions - ); - const filters = combineRangeWithFilters(dateRangeStart, dateRangeEnd, search); - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - size: 0, - query: { - ...filters, - }, - aggs, - }, - }; - - const { aggregations } = await callES('search', params); - return extractFilterAggsResults(aggregations, ['tags', 'locations', 'ports', 'schemes']); - }, - - getMonitorDetails: async ({ callES, monitorId, dateStart, dateEnd }) => { - const queryFilters: any = [ - { - range: { - '@timestamp': { - gte: dateStart, - lte: dateEnd, - }, - }, - }, - { - term: { - 'monitor.id': monitorId, - }, - }, - ]; - - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - size: 1, - _source: ['error', '@timestamp'], - query: { - bool: { - must: [ - { - exists: { - field: 'error', - }, - }, - ], - filter: queryFilters, - }, - }, - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - }, - }; - - const result = await callES('search', params); - - const data = result.hits.hits[0]?._source; - - const monitorError: MonitorError | undefined = data?.error; - const errorTimeStamp: string | undefined = data?.['@timestamp']; - - return { - monitorId, - error: monitorError, - timestamp: errorTimeStamp, - }; - }, - - getMonitorLocations: async ({ callES, monitorId, dateStart, dateEnd }) => { - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - size: 0, - query: { - bool: { - filter: [ - { - match: { - 'monitor.id': monitorId, - }, - }, - { - exists: { - field: 'summary', - }, - }, - { - range: { - '@timestamp': { - gte: dateStart, - lte: dateEnd, - }, - }, - }, - ], - }, - }, - aggs: { - location: { - terms: { - field: 'observer.geo.name', - missing: '__location_missing__', - }, - aggs: { - most_recent: { - top_hits: { - size: 1, - sort: { - '@timestamp': { - order: 'desc', - }, - }, - _source: ['monitor', 'summary', 'observer', '@timestamp'], - }, - }, - }, - }, - }, - }, - }; - - const result = await callES('search', params); - const locations = result?.aggregations?.location?.buckets ?? []; - - const getGeo = (locGeo: { name: string; location?: string }) => { - if (locGeo) { - const { name, location } = locGeo; - const latLon = location?.trim().split(','); - return { - name, - location: latLon - ? { - lat: latLon[0], - lon: latLon[1], - } - : undefined, - }; - } else { - return { - name: UNNAMED_LOCATION, - }; - } - }; - - const monLocs: MonitorLocation[] = []; - locations.forEach((loc: any) => { - const mostRecentLocation = loc.most_recent.hits.hits[0]._source; - const location: MonitorLocation = { - summary: mostRecentLocation?.summary, - geo: getGeo(mostRecentLocation?.observer?.geo), - timestamp: mostRecentLocation['@timestamp'], - }; - monLocs.push(location); - }); - - return { - monitorId, - locations: monLocs, - }; - }, -}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts deleted file mode 100644 index 8422ac5cd1ee8..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts +++ /dev/null @@ -1,495 +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 { set } from 'lodash'; -import { elasticsearchPingsAdapter as adapter } from '../es_pings'; - -describe('ElasticsearchPingsAdapter class', () => { - let mockHits: any[]; - let mockEsSearchResult: any; - let mockEsCountResult: any; - const standardMockResponse: any = { - aggregations: { - timeseries: { - buckets: [ - { - key: 1, - up: { - doc_count: 2, - }, - down: { - doc_count: 1, - }, - }, - { - key: 2, - up: { - doc_count: 2, - }, - down: { - bucket_count: 1, - }, - }, - ], - interval: '1s', - }, - }, - }; - - beforeEach(() => { - mockHits = [ - { - _source: { - '@timestamp': '2018-10-30T18:51:59.792Z', - }, - }, - { - _source: { - '@timestamp': '2018-10-30T18:53:59.792Z', - }, - }, - { - _source: { - '@timestamp': '2018-10-30T18:55:59.792Z', - }, - }, - ]; - mockEsSearchResult = { - hits: { - total: { - value: mockHits.length, - }, - hits: mockHits, - }, - aggregations: { - locations: { - buckets: [{ key: 'foo' }], - }, - }, - }; - mockEsCountResult = { - count: mockHits.length, - }; - }); - - describe('getPingHistogram', () => { - it('returns a single bucket if array has 1', async () => { - expect.assertions(2); - const mockEsClient = jest.fn(); - mockEsClient.mockReturnValue({ - aggregations: { - timeseries: { - buckets: [ - { - key: 1, - up: { - doc_count: 2, - }, - down: { - doc_count: 1, - }, - }, - ], - }, - }, - }); - const result = await adapter.getPingHistogram({ - callES: mockEsClient, - dateStart: 'now-15m', - dateEnd: 'now', - filters: '', - }); - result.interval = '10s'; - expect(mockEsClient).toHaveBeenCalledTimes(1); - expect(result).toMatchSnapshot(); - }); - - it('returns expected result for no status filter', async () => { - expect.assertions(2); - const mockEsClient = jest.fn(); - - mockEsClient.mockReturnValue(standardMockResponse); - - const result = await adapter.getPingHistogram({ - callES: mockEsClient, - dateStart: 'now-15m', - dateEnd: 'now', - filters: '', - }); - result.interval = '1m'; - - expect(mockEsClient).toHaveBeenCalledTimes(1); - expect(result).toMatchSnapshot(); - }); - - it('handles status + additional user queries', async () => { - expect.assertions(2); - const mockEsClient = jest.fn(); - mockEsClient.mockReturnValue({ - aggregations: { - timeseries: { - buckets: [ - { - key: 1, - up: { - doc_count: 2, - }, - down: { - doc_count: 1, - }, - }, - { - key: 2, - up: { - doc_count: 2, - }, - down: { - doc_count: 2, - }, - }, - { - key: 3, - up: { - doc_count: 3, - }, - down: { - doc_count: 1, - }, - }, - ], - }, - }, - }); - const searchFilter = { - bool: { - must: [ - { match: { 'monitor.id': { query: 'auto-http-0X89BB0F9A6C81D178', operator: 'and' } } }, - { match: { 'monitor.name': { query: 'my-new-test-site-name', operator: 'and' } } }, - ], - }, - }; - const result = await adapter.getPingHistogram({ - callES: mockEsClient, - dateStart: '1234', - dateEnd: '5678', - filters: JSON.stringify(searchFilter), - monitorId: undefined, - statusFilter: 'down', - }); - result.interval = '1h'; - - expect(mockEsClient).toHaveBeenCalledTimes(1); - expect(result).toMatchSnapshot(); - }); - - it('handles simple_text_query without issues', async () => { - expect.assertions(2); - const mockEsClient = jest.fn(); - mockEsClient.mockReturnValue({ - aggregations: { - timeseries: { - buckets: [ - { - key: 1, - up: { - doc_count: 2, - }, - down: { - doc_count: 1, - }, - }, - { - key: 2, - up: { - doc_count: 1, - }, - down: { - doc_count: 2, - }, - }, - { - key: 3, - up: { - doc_count: 3, - }, - down: { - doc_count: 1, - }, - }, - ], - }, - }, - }); - const filters = `{"bool":{"must":[{"simple_query_string":{"query":"http"}}]}}`; - const result = await adapter.getPingHistogram({ - callES: mockEsClient, - dateStart: 'now-15m', - dateEnd: 'now', - filters, - }); - - result.interval = '1m'; - expect(mockEsClient).toHaveBeenCalledTimes(1); - expect(result).toMatchSnapshot(); - }); - - it('returns a down-filtered array for when filtered by down status', async () => { - expect.assertions(2); - const mockEsClient = jest.fn(); - mockEsClient.mockReturnValue(standardMockResponse); - const result = await adapter.getPingHistogram({ - callES: mockEsClient, - dateStart: '1234', - dateEnd: '5678', - filters: '', - monitorId: undefined, - statusFilter: 'down', - }); - - result.interval = '1d'; - - expect(mockEsClient).toHaveBeenCalledTimes(1); - expect(result).toMatchSnapshot(); - }); - - it('returns a down-filtered array for when filtered by up status', async () => { - expect.assertions(2); - const mockEsClient = jest.fn(); - - mockEsClient.mockReturnValue(standardMockResponse); - - const result = await adapter.getPingHistogram({ - callES: mockEsClient, - dateStart: '1234', - dateEnd: '5678', - filters: '', - monitorId: undefined, - statusFilter: 'up', - }); - - expect(mockEsClient).toHaveBeenCalledTimes(1); - expect(result).toMatchSnapshot(); - }); - }); - - describe('getDocCount', () => { - it('returns data in appropriate shape', async () => { - const mockEsClient = jest.fn(); - mockEsClient.mockReturnValue(mockEsCountResult); - const { count } = await adapter.getDocCount({ callES: mockEsClient }); - expect(count).toEqual(3); - }); - }); - - describe('getAll', () => { - let expectedGetAllParams: any; - beforeEach(() => { - expectedGetAllParams = { - index: 'heartbeat*', - body: { - query: { - bool: { - filter: [{ range: { '@timestamp': { gte: 'now-1h', lte: 'now' } } }], - }, - }, - aggregations: { - locations: { - terms: { - field: 'observer.geo.name', - missing: 'N/A', - size: 1000, - }, - }, - }, - sort: [{ '@timestamp': { order: 'desc' } }], - size: 12, - }, - }; - }); - - it('returns data in the appropriate shape', async () => { - const mockEsClient = jest.fn(); - mockEsClient.mockReturnValue(mockEsSearchResult); - const result = await adapter.getAll({ - callES: mockEsClient, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', - sort: 'asc', - size: 12, - }); - const count = 3; - - expect(result.total).toBe(count); - - const pings = result.pings!; - expect(pings).toHaveLength(count); - expect(pings[0].timestamp).toBe('2018-10-30T18:51:59.792Z'); - expect(pings[1].timestamp).toBe('2018-10-30T18:53:59.792Z'); - expect(pings[2].timestamp).toBe('2018-10-30T18:55:59.792Z'); - expect(mockEsClient).toHaveBeenCalledTimes(1); - }); - - it('creates appropriate sort and size parameters', async () => { - const mockEsClient = jest.fn(); - mockEsClient.mockReturnValue(mockEsSearchResult); - await adapter.getAll({ - callES: mockEsClient, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', - sort: 'asc', - size: 12, - }); - set(expectedGetAllParams, 'body.sort[0]', { '@timestamp': { order: 'asc' } }); - - expect(mockEsClient).toHaveBeenCalledTimes(1); - expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); - }); - - it('omits the sort param when no sort passed', async () => { - const mockEsClient = jest.fn(); - mockEsClient.mockReturnValue(mockEsSearchResult); - await adapter.getAll({ - callES: mockEsClient, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', - size: 12, - }); - - expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); - }); - - it('omits the size param when no size passed', async () => { - const mockEsClient = jest.fn(); - mockEsClient.mockReturnValue(mockEsSearchResult); - await adapter.getAll({ - callES: mockEsClient, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', - sort: 'desc', - }); - delete expectedGetAllParams.body.size; - set(expectedGetAllParams, 'body.sort[0].@timestamp.order', 'desc'); - - expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); - }); - - it('adds a filter for monitor ID', async () => { - const mockEsClient = jest.fn(); - mockEsClient.mockReturnValue(mockEsSearchResult); - await adapter.getAll({ - callES: mockEsClient, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', - monitorId: 'testmonitorid', - }); - delete expectedGetAllParams.body.size; - expectedGetAllParams.body.query.bool.filter.push({ term: { 'monitor.id': 'testmonitorid' } }); - - expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); - }); - - it('adds a filter for monitor status', async () => { - const mockEsClient = jest.fn(); - mockEsClient.mockReturnValue(mockEsSearchResult); - await adapter.getAll({ - callES: mockEsClient, - dateRangeStart: 'now-1h', - dateRangeEnd: 'now', - status: 'down', - }); - delete expectedGetAllParams.body.size; - expectedGetAllParams.body.query.bool.filter.push({ term: { 'monitor.status': 'down' } }); - - expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); - }); - }); - - describe('getLatestMonitorStatus', () => { - let expectedGetLatestSearchParams: any; - beforeEach(() => { - expectedGetLatestSearchParams = { - index: 'heartbeat*', - body: { - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now-1h', - lte: 'now', - }, - }, - }, - { - term: { 'monitor.id': 'testMonitor' }, - }, - ], - }, - }, - aggs: { - by_id: { - terms: { - field: 'monitor.id', - size: 1000, - }, - aggs: { - latest: { - top_hits: { - size: 1, - sort: { - '@timestamp': { order: 'desc' }, - }, - }, - }, - }, - }, - }, - size: 0, - }, - }; - mockEsSearchResult = { - aggregations: { - by_id: { - buckets: [ - { - latest: { - hits: { - hits: [ - { - _source: { - '@timestamp': 123456, - monitor: { - id: 'testMonitor', - }, - }, - }, - ], - }, - }, - }, - ], - }, - }, - }; - }); - - it('returns data in expected shape', async () => { - const mockEsClient = jest.fn(async (_request: any, _params: any) => mockEsSearchResult); - const result = await adapter.getLatestMonitorStatus({ - callES: mockEsClient, - dateStart: 'now-1h', - dateEnd: 'now', - monitorId: 'testMonitor', - }); - expect(result.timestamp).toBe(123456); - expect(result.monitor).not.toBeFalsy(); - // @ts-ignore monitor will be defined - expect(result.monitor.id).toBe('testMonitor'); - expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetLatestSearchParams); - }); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/es_pings.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/es_pings.ts deleted file mode 100644 index 93e3a1bd9397b..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/es_pings.ts +++ /dev/null @@ -1,183 +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 { get } from 'lodash'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { HttpBody, Ping, PingResults } from '../../../../common/graphql/types'; -import { UMPingsAdapter } from './types'; -import { esGetPingHistogram } from './es_get_ping_historgram'; - -export const elasticsearchPingsAdapter: UMPingsAdapter = { - getAll: async ({ - callES, - dateRangeStart, - dateRangeEnd, - monitorId, - status, - sort, - size, - location, - }) => { - const sortParam = { sort: [{ '@timestamp': { order: sort ?? 'desc' } }] }; - const sizeParam = size ? { size } : undefined; - const filter: any[] = [{ range: { '@timestamp': { gte: dateRangeStart, lte: dateRangeEnd } } }]; - if (monitorId) { - filter.push({ term: { 'monitor.id': monitorId } }); - } - if (status) { - filter.push({ term: { 'monitor.status': status } }); - } - - let postFilterClause = {}; - if (location) { - postFilterClause = { post_filter: { term: { 'observer.geo.name': location } } }; - } - const queryContext = { bool: { filter } }; - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - query: { - ...queryContext, - }, - ...sortParam, - ...sizeParam, - aggregations: { - locations: { - terms: { - field: 'observer.geo.name', - missing: 'N/A', - size: 1000, - }, - }, - }, - ...postFilterClause, - }, - }; - - const { - hits: { hits, total }, - aggregations: aggs, - } = await callES('search', params); - - const locations = get(aggs, 'locations', { buckets: [{ key: 'N/A', doc_count: 0 }] }); - - const pings: Ping[] = hits.map(({ _id, _source }: any) => { - const timestamp = _source['@timestamp']; - - // Calculate here the length of the content string in bytes, this is easier than in client JS, where - // we don't have access to Buffer.byteLength. There are some hacky ways to do this in the - // client but this is cleaner. - const httpBody = get(_source, 'http.response.body'); - if (httpBody && httpBody.content) { - httpBody.content_bytes = Buffer.byteLength(httpBody.content); - } - - return { id: _id, timestamp, ..._source }; - }); - - const results: PingResults = { - total: total.value, - locations: locations.buckets.map((bucket: { key: string }) => bucket.key), - pings, - }; - - return results; - }, - - // Get The monitor latest state sorted by timestamp with date range - getLatestMonitorStatus: async ({ callES, dateStart, dateEnd, monitorId }) => { - // TODO: Write tests for this function - - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: dateStart, - lte: dateEnd, - }, - }, - }, - ...(monitorId ? [{ term: { 'monitor.id': monitorId } }] : []), - ], - }, - }, - size: 0, - aggs: { - by_id: { - terms: { - field: 'monitor.id', - size: 1000, - }, - aggs: { - latest: { - top_hits: { - size: 1, - sort: { - '@timestamp': { order: 'desc' }, - }, - }, - }, - }, - }, - }, - }, - }; - - const result = await callES('search', params); - const ping: any = result.aggregations.by_id.buckets?.[0]?.latest.hits?.hits?.[0] ?? {}; - - return { - ...ping?._source, - timestamp: ping?._source?.['@timestamp'], - }; - }, - - // Get the monitor meta info regardless of timestamp - getMonitor: async ({ callES, monitorId }) => { - const params = { - index: INDEX_NAMES.HEARTBEAT, - body: { - size: 1, - _source: ['url', 'monitor', 'observer'], - query: { - bool: { - filter: [ - { - term: { - 'monitor.id': monitorId, - }, - }, - ], - }, - }, - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - }, - }; - - const result = await callES('search', params); - - return result.hits.hits[0]?._source; - }, - - getPingHistogram: esGetPingHistogram, - - getDocCount: async ({ callES }) => { - const { count } = await callES('count', { index: INDEX_NAMES.HEARTBEAT }); - - return { count }; - }, -}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/stub_index_pattern/index.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/stub_index_pattern/index.ts deleted file mode 100644 index 4ef6e3fa8a6bd..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/stub_index_pattern/index.ts +++ /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 { StubIndexPatternAdapter, stubIndexPatternAdapter } from './stub_index_pattern'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/stub_index_pattern/stub_index_pattern.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/stub_index_pattern/stub_index_pattern.ts deleted file mode 100644 index 49ec86af25040..0000000000000 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/stub_index_pattern/stub_index_pattern.ts +++ /dev/null @@ -1,54 +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 { APICaller } from 'kibana/server'; -import { - IndexPatternsFetcher, - IIndexPattern, -} from '../../../../../../../../src/plugins/data/server'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { UMElasticsearchQueryFn } from '../framework'; - -export interface StubIndexPatternAdapter { - getUptimeIndexPattern: UMElasticsearchQueryFn; -} - -export const stubIndexPatternAdapter: StubIndexPatternAdapter = { - getUptimeIndexPattern: async callES => { - const indexPatternsFetcher = new IndexPatternsFetcher((...rest: Parameters) => - callES(...rest) - ); - - // Since `getDynamicIndexPattern` is called in setup_request (and thus by every endpoint) - // and since `getFieldsForWildcard` will throw if the specified indices don't exist, - // we have to catch errors here to avoid all endpoints returning 500 for users without APM data - // (would be a bad first time experience) - try { - const fields = await indexPatternsFetcher.getFieldsForWildcard({ - pattern: INDEX_NAMES.HEARTBEAT, - }); - - const indexPattern: IIndexPattern = { - fields, - title: INDEX_NAMES.HEARTBEAT, - }; - - return indexPattern; - } catch (e) { - const notExists = e.output?.statusCode === 404; - if (notExists) { - // eslint-disable-next-line no-console - console.error( - `Could not get dynamic index pattern because indices "${INDEX_NAMES.HEARTBEAT}" don't exist` - ); - return; - } - - // re-throw - throw e; - } - }, -}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts index b44a890de3819..875a5d9dc8c5c 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/compose/kibana.ts @@ -5,23 +5,19 @@ */ import { UMKibanaBackendFrameworkAdapter } from '../adapters/framework'; -import { elasticsearchMonitorsAdapter } from '../adapters/monitors'; -import { elasticsearchPingsAdapter } from '../adapters/pings'; +import * as requests from '../requests'; import { licenseCheck } from '../domains'; import { UMDomainLibs, UMServerLibs } from '../lib'; -import { elasticsearchMonitorStatesAdapter } from '../adapters/monitor_states'; -import { stubIndexPatternAdapter } from '../adapters/stub_index_pattern'; import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters/framework'; export function compose(server: UptimeCoreSetup, plugins: UptimeCorePlugins): UMServerLibs { const framework = new UMKibanaBackendFrameworkAdapter(server); const domainLibs: UMDomainLibs = { + requests: { + ...requests, + }, license: licenseCheck, - monitors: elasticsearchMonitorsAdapter, - monitorStates: elasticsearchMonitorStatesAdapter, - pings: elasticsearchPingsAdapter, - stubIndexPattern: stubIndexPatternAdapter, }; return { diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts index eae8023b66ff4..0dcbc0a424b5e 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts @@ -5,6 +5,7 @@ */ export { getFilterClause } from './get_filter_clause'; +export { parseRelativeDate } from './get_histogram_interval'; export { getHistogramIntervalFormatted } from './get_histogram_interval_formatted'; export { parseFilterQuery } from './parse_filter_query'; export { assertCloseTo } from './assert_close_to'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/lib.ts b/x-pack/legacy/plugins/uptime/server/lib/lib.ts index e5ab9940d482d..a7121eaec6679 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/lib.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/lib.ts @@ -4,21 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - UMBackendFrameworkAdapter, - UMMonitorsAdapter, - UMMonitorStatesAdapter, - UMPingsAdapter, - StubIndexPatternAdapter, -} from './adapters'; +import { UMBackendFrameworkAdapter } from './adapters'; import { UMLicenseCheck } from './domains'; +import { UptimeRequests } from './requests'; export interface UMDomainLibs { + requests: UptimeRequests; license: UMLicenseCheck; - monitors: UMMonitorsAdapter; - monitorStates: UMMonitorStatesAdapter; - pings: UMPingsAdapter; - stubIndexPattern: StubIndexPatternAdapter; } export interface UMServerLibs extends UMDomainLibs { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap rename to x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/extract_filter_aggs_results.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap rename to x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/generate_filter_aggs.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/elasticsearch_monitors_adapter.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/__snapshots__/elasticsearch_monitors_adapter.test.ts.snap rename to x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_monitor_charts.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/__snapshots__/elasticsearch_pings_adapter.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_ping_histogram.test.ts.snap similarity index 70% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/__snapshots__/elasticsearch_pings_adapter.test.ts.snap rename to x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_ping_histogram.test.ts.snap index 1b31f44557df0..0dafa5144c25a 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/__snapshots__/elasticsearch_pings_adapter.test.ts.snap +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/__snapshots__/get_ping_histogram.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ElasticsearchPingsAdapter class getPingHistogram handles simple_text_query without issues 1`] = ` +exports[`getPingHistogram handles simple_text_query without issues 1`] = ` Object { "histogram": Array [ Object { @@ -26,7 +26,7 @@ Object { } `; -exports[`ElasticsearchPingsAdapter class getPingHistogram handles status + additional user queries 1`] = ` +exports[`getPingHistogram handles status + additional user queries 1`] = ` Object { "histogram": Array [ Object { @@ -52,7 +52,7 @@ Object { } `; -exports[`ElasticsearchPingsAdapter class getPingHistogram returns a down-filtered array for when filtered by down status 1`] = ` +exports[`getPingHistogram returns a down-filtered array for when filtered by down status 1`] = ` Object { "histogram": Array [ Object { @@ -72,7 +72,7 @@ Object { } `; -exports[`ElasticsearchPingsAdapter class getPingHistogram returns a down-filtered array for when filtered by up status 1`] = ` +exports[`getPingHistogram returns a down-filtered array for when filtered by up status 1`] = ` Object { "histogram": Array [ Object { @@ -92,7 +92,7 @@ Object { } `; -exports[`ElasticsearchPingsAdapter class getPingHistogram returns a single bucket if array has 1 1`] = ` +exports[`getPingHistogram returns a single bucket if array has 1 1`] = ` Object { "histogram": Array [ Object { @@ -106,7 +106,7 @@ Object { } `; -exports[`ElasticsearchPingsAdapter class getPingHistogram returns expected result for no status filter 1`] = ` +exports[`getPingHistogram returns expected result for no status filter 1`] = ` Object { "histogram": Array [ Object { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/combine_range_with_filters.test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/combine_range_with_filters.test.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/combine_range_with_filters.test.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/combine_range_with_filters.test.ts index 2075b3a8fbe0f..31d19dab290da 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/combine_range_with_filters.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/combine_range_with_filters.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { combineRangeWithFilters } from '../elasticsearch_monitors_adapter'; +import { combineRangeWithFilters } from '../get_filter_bar'; describe('combineRangeWithFilters', () => { it('combines filters that have no filter clause', () => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/extract_filter_aggs_results.test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/extract_filter_aggs_results.test.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts index 954cffd4c9522..19fd0fda8d83e 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/extract_filter_aggs_results.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/extract_filter_aggs_results.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { extractFilterAggsResults } from '../elasticsearch_monitors_adapter'; +import { extractFilterAggsResults } from '../get_filter_bar'; describe('extractFilterAggsResults', () => { it('extracts the bucket values of the expected filter fields', () => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/generate_filter_aggs.test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/generate_filter_aggs.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/generate_filter_aggs.test.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/generate_filter_aggs.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_doc_count.test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_doc_count.test.ts new file mode 100644 index 0000000000000..7dfb0314fe8dd --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_doc_count.test.ts @@ -0,0 +1,42 @@ +/* + * 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 { getDocCount } from '../get_doc_count'; + +describe('getDocCount', () => { + let mockHits: any[]; + let mockEsCountResult: any; + + beforeEach(() => { + mockHits = [ + { + _source: { + '@timestamp': '2018-10-30T18:51:59.792Z', + }, + }, + { + _source: { + '@timestamp': '2018-10-30T18:53:59.792Z', + }, + }, + { + _source: { + '@timestamp': '2018-10-30T18:55:59.792Z', + }, + }, + ]; + mockEsCountResult = { + count: mockHits.length, + }; + }); + + it('returns data in appropriate shape', async () => { + const mockEsClient = jest.fn(); + mockEsClient.mockReturnValue(mockEsCountResult); + const { count } = await getDocCount({ callES: mockEsClient }); + expect(count).toEqual(3); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts new file mode 100644 index 0000000000000..49771c4812972 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { getLatestMonitor } from '../get_latest_monitor'; + +describe('getLatestMonitor', () => { + let expectedGetLatestSearchParams: any; + let mockEsSearchResult: any; + beforeEach(() => { + expectedGetLatestSearchParams = { + index: 'heartbeat*', + body: { + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: 'now-1h', + lte: 'now', + }, + }, + }, + { + term: { 'monitor.id': 'testMonitor' }, + }, + ], + }, + }, + aggs: { + by_id: { + terms: { + field: 'monitor.id', + size: 1000, + }, + aggs: { + latest: { + top_hits: { + size: 1, + sort: { + '@timestamp': { order: 'desc' }, + }, + }, + }, + }, + }, + }, + size: 0, + }, + }; + mockEsSearchResult = { + aggregations: { + by_id: { + buckets: [ + { + latest: { + hits: { + hits: [ + { + _source: { + '@timestamp': 123456, + monitor: { + id: 'testMonitor', + }, + }, + }, + ], + }, + }, + }, + ], + }, + }, + }; + }); + + it('returns data in expected shape', async () => { + const mockEsClient = jest.fn(async (_request: any, _params: any) => mockEsSearchResult); + const result = await getLatestMonitor({ + callES: mockEsClient, + dateStart: 'now-1h', + dateEnd: 'now', + monitorId: 'testMonitor', + }); + expect(result.timestamp).toBe(123456); + expect(result.monitor).not.toBeFalsy(); + expect(result?.monitor?.id).toBe('testMonitor'); + expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetLatestSearchParams); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/elasticsearch_monitors_adapter.test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts similarity index 88% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/elasticsearch_monitors_adapter.test.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts index e3e81fe360718..205f9cf745db1 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/elasticsearch_monitors_adapter.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts @@ -5,17 +5,16 @@ */ import { get, set } from 'lodash'; -import { elasticsearchMonitorsAdapter as adapter } from '../elasticsearch_monitors_adapter'; import mockChartsData from './monitor_charts_mock.json'; -import { assertCloseTo } from '../../../helper'; +import { assertCloseTo } from '../../helper'; +import { getMonitorCharts } from '../get_monitor_charts'; -// FIXME: there are many untested functions in this adapter. They should be tested. describe('ElasticsearchMonitorsAdapter', () => { it('getMonitorChartsData will run expected parameters when no location is specified', async () => { expect.assertions(3); const searchMock = jest.fn(); const search = searchMock.bind({}); - await adapter.getMonitorChartsData({ + await getMonitorCharts({ callES: search, monitorId: 'fooID', dateRangeStart: 'now-15m', @@ -50,7 +49,7 @@ describe('ElasticsearchMonitorsAdapter', () => { expect.assertions(3); const searchMock = jest.fn(); const search = searchMock.bind({}); - await adapter.getMonitorChartsData({ + await getMonitorCharts({ callES: search, monitorId: 'fooID', dateRangeStart: 'now-15m', @@ -87,7 +86,7 @@ describe('ElasticsearchMonitorsAdapter', () => { searchMock.mockReturnValue(mockChartsData); const search = searchMock.bind({}); expect( - await adapter.getMonitorChartsData({ + await getMonitorCharts({ callES: search, monitorId: 'id', dateRangeStart: 'now-15m', diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts new file mode 100644 index 0000000000000..7d98b77069264 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts @@ -0,0 +1,241 @@ +/* + * 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 { getPingHistogram } from '../get_ping_histogram'; + +describe('getPingHistogram', () => { + const standardMockResponse: any = { + aggregations: { + timeseries: { + buckets: [ + { + key: 1, + up: { + doc_count: 2, + }, + down: { + doc_count: 1, + }, + }, + { + key: 2, + up: { + doc_count: 2, + }, + down: { + bucket_count: 1, + }, + }, + ], + }, + }, + }; + + it('returns a single bucket if array has 1', async () => { + expect.assertions(2); + const mockEsClient = jest.fn(); + mockEsClient.mockReturnValue({ + aggregations: { + timeseries: { + buckets: [ + { + key: 1, + up: { + doc_count: 2, + }, + down: { + doc_count: 1, + }, + }, + ], + interval: '10s', + }, + }, + }); + + const result = await getPingHistogram({ + callES: mockEsClient, + from: 'now-15m', + to: 'now', + filters: null, + }); + + expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(result).toMatchSnapshot(); + }); + + it('returns expected result for no status filter', async () => { + expect.assertions(2); + const mockEsClient = jest.fn(); + + standardMockResponse.aggregations.timeseries.interval = '1m'; + mockEsClient.mockReturnValue(standardMockResponse); + + const result = await getPingHistogram({ + callES: mockEsClient, + from: 'now-15m', + to: 'now', + filters: null, + }); + + expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(result).toMatchSnapshot(); + }); + + it('handles status + additional user queries', async () => { + expect.assertions(2); + const mockEsClient = jest.fn(); + + mockEsClient.mockReturnValue({ + aggregations: { + timeseries: { + buckets: [ + { + key: 1, + up: { + doc_count: 2, + }, + down: { + doc_count: 1, + }, + }, + { + key: 2, + up: { + doc_count: 2, + }, + down: { + doc_count: 2, + }, + }, + { + key: 3, + up: { + doc_count: 3, + }, + down: { + doc_count: 1, + }, + }, + ], + interval: '1h', + }, + }, + }); + + const searchFilter = { + bool: { + must: [ + { match: { 'monitor.id': { query: 'auto-http-0X89BB0F9A6C81D178', operator: 'and' } } }, + { match: { 'monitor.name': { query: 'my-new-test-site-name', operator: 'and' } } }, + ], + }, + }; + + const result = await getPingHistogram({ + callES: mockEsClient, + from: '1234', + to: '5678', + filters: JSON.stringify(searchFilter), + monitorId: undefined, + statusFilter: 'down', + }); + + expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(result).toMatchSnapshot(); + }); + + it('handles simple_text_query without issues', async () => { + expect.assertions(2); + const mockEsClient = jest.fn(); + + mockEsClient.mockReturnValue({ + aggregations: { + timeseries: { + buckets: [ + { + key: 1, + up: { + doc_count: 2, + }, + down: { + doc_count: 1, + }, + }, + { + key: 2, + up: { + doc_count: 1, + }, + down: { + doc_count: 2, + }, + }, + { + key: 3, + up: { + doc_count: 3, + }, + down: { + doc_count: 1, + }, + }, + ], + interval: '1m', + }, + }, + }); + + const filters = `{"bool":{"must":[{"simple_query_string":{"query":"http"}}]}}`; + const result = await getPingHistogram({ + callES: mockEsClient, + from: 'now-15m', + to: 'now', + filters, + }); + + expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(result).toMatchSnapshot(); + }); + + it('returns a down-filtered array for when filtered by down status', async () => { + expect.assertions(2); + const mockEsClient = jest.fn(); + standardMockResponse.aggregations.timeseries.interval = '1d'; + mockEsClient.mockReturnValue(standardMockResponse); + const result = await getPingHistogram({ + callES: mockEsClient, + from: '1234', + to: '5678', + filters: '', + monitorId: undefined, + statusFilter: 'down', + }); + + expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(result).toMatchSnapshot(); + }); + + it('returns a down-filtered array for when filtered by up status', async () => { + expect.assertions(2); + const mockEsClient = jest.fn(); + + standardMockResponse.aggregations.timeseries.interval = '1s'; + mockEsClient.mockReturnValue(standardMockResponse); + + const result = await getPingHistogram({ + callES: mockEsClient, + from: '1234', + to: '5678', + filters: '', + monitorId: undefined, + statusFilter: 'up', + }); + + expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts new file mode 100644 index 0000000000000..4be1fb30ee7f6 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts @@ -0,0 +1,163 @@ +/* + * 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 { getPings } from '../get_pings'; +import { set } from 'lodash'; + +describe('getAll', () => { + let mockEsSearchResult: any; + let mockHits: any; + let expectedGetAllParams: any; + beforeEach(() => { + mockHits = [ + { + _source: { + '@timestamp': '2018-10-30T18:51:59.792Z', + }, + }, + { + _source: { + '@timestamp': '2018-10-30T18:53:59.792Z', + }, + }, + { + _source: { + '@timestamp': '2018-10-30T18:55:59.792Z', + }, + }, + ]; + mockEsSearchResult = { + hits: { + total: { + value: mockHits.length, + }, + hits: mockHits, + }, + aggregations: { + locations: { + buckets: [{ key: 'foo' }], + }, + }, + }; + expectedGetAllParams = { + index: 'heartbeat*', + body: { + query: { + bool: { + filter: [{ range: { '@timestamp': { gte: 'now-1h', lte: 'now' } } }], + }, + }, + aggregations: { + locations: { + terms: { + field: 'observer.geo.name', + missing: 'N/A', + size: 1000, + }, + }, + }, + sort: [{ '@timestamp': { order: 'desc' } }], + size: 12, + }, + }; + }); + + it('returns data in the appropriate shape', async () => { + const mockEsClient = jest.fn(); + mockEsClient.mockReturnValue(mockEsSearchResult); + const result = await getPings({ + callES: mockEsClient, + dateRangeStart: 'now-1h', + dateRangeEnd: 'now', + sort: 'asc', + size: 12, + }); + const count = 3; + + expect(result.total).toBe(count); + + const pings = result.pings!; + expect(pings).toHaveLength(count); + expect(pings[0].timestamp).toBe('2018-10-30T18:51:59.792Z'); + expect(pings[1].timestamp).toBe('2018-10-30T18:53:59.792Z'); + expect(pings[2].timestamp).toBe('2018-10-30T18:55:59.792Z'); + expect(mockEsClient).toHaveBeenCalledTimes(1); + }); + + it('creates appropriate sort and size parameters', async () => { + const mockEsClient = jest.fn(); + mockEsClient.mockReturnValue(mockEsSearchResult); + await getPings({ + callES: mockEsClient, + dateRangeStart: 'now-1h', + dateRangeEnd: 'now', + sort: 'asc', + size: 12, + }); + set(expectedGetAllParams, 'body.sort[0]', { '@timestamp': { order: 'asc' } }); + + expect(mockEsClient).toHaveBeenCalledTimes(1); + expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); + }); + + it('omits the sort param when no sort passed', async () => { + const mockEsClient = jest.fn(); + mockEsClient.mockReturnValue(mockEsSearchResult); + await getPings({ + callES: mockEsClient, + dateRangeStart: 'now-1h', + dateRangeEnd: 'now', + size: 12, + }); + + expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); + }); + + it('omits the size param when no size passed', async () => { + const mockEsClient = jest.fn(); + mockEsClient.mockReturnValue(mockEsSearchResult); + await getPings({ + callES: mockEsClient, + dateRangeStart: 'now-1h', + dateRangeEnd: 'now', + sort: 'desc', + }); + delete expectedGetAllParams.body.size; + set(expectedGetAllParams, 'body.sort[0].@timestamp.order', 'desc'); + + expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); + }); + + it('adds a filter for monitor ID', async () => { + const mockEsClient = jest.fn(); + mockEsClient.mockReturnValue(mockEsSearchResult); + await getPings({ + callES: mockEsClient, + dateRangeStart: 'now-1h', + dateRangeEnd: 'now', + monitorId: 'testmonitorid', + }); + delete expectedGetAllParams.body.size; + expectedGetAllParams.body.query.bool.filter.push({ term: { 'monitor.id': 'testmonitorid' } }); + + expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); + }); + + it('adds a filter for monitor status', async () => { + const mockEsClient = jest.fn(); + mockEsClient.mockReturnValue(mockEsSearchResult); + await getPings({ + callES: mockEsClient, + dateRangeStart: 'now-1h', + dateRangeEnd: 'now', + status: 'down', + }); + delete expectedGetAllParams.body.size; + expectedGetAllParams.body.query.bool.filter.push({ term: { 'monitor.status': 'down' } }); + + expect(mockEsClient).toHaveBeenCalledWith('search', expectedGetAllParams); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/monitor_charts_mock.json b/x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/monitor_charts_mock.json rename to x-pack/legacy/plugins/uptime/server/lib/requests/__tests__/monitor_charts_mock.json diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/generate_filter_aggs.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/generate_filter_aggs.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/generate_filter_aggs.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/generate_filter_aggs.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_doc_count.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_doc_count.ts new file mode 100644 index 0000000000000..68c122aaaa9fb --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_doc_count.ts @@ -0,0 +1,15 @@ +/* + * 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 { DocCount } from '../../../common/graphql/types'; +import { INDEX_NAMES } from '../../../common/constants'; +import { UMElasticsearchQueryFn } from '../adapters'; + +export const getDocCount: UMElasticsearchQueryFn<{}, DocCount> = async ({ callES }) => { + const { count } = await callES('count', { index: INDEX_NAMES.HEARTBEAT }); + + return { count }; +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_filter_bar.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_filter_bar.ts new file mode 100644 index 0000000000000..79259afe2b9eb --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_filter_bar.ts @@ -0,0 +1,98 @@ +/* + * 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 { UMElasticsearchQueryFn } from '../adapters'; +import { OverviewFilters } from '../../../common/runtime_types'; +import { generateFilterAggs } from './generate_filter_aggs'; +import { INDEX_NAMES } from '../../../common/constants'; + +export interface GetFilterBarParams { + /** @param dateRangeStart timestamp bounds */ + dateRangeStart: string; + /** @member dateRangeEnd timestamp bounds */ + dateRangeEnd: string; + /** @member search this value should correspond to Elasticsearch DSL + * generated from KQL text the user provided. + */ + search?: Record; + filterOptions: Record; +} + +export const combineRangeWithFilters = ( + dateRangeStart: string, + dateRangeEnd: string, + filters?: Record +) => { + const range = { + range: { + '@timestamp': { + gte: dateRangeStart, + lte: dateRangeEnd, + }, + }, + }; + if (!filters) return range; + const clientFiltersList = Array.isArray(filters?.bool?.filter ?? {}) + ? // i.e. {"bool":{"filter":{ ...some nested filter objects }}} + filters.bool.filter + : // i.e. {"bool":{"filter":[ ...some listed filter objects ]}} + Object.keys(filters?.bool?.filter ?? {}).map(key => ({ + ...filters?.bool?.filter?.[key], + })); + filters.bool.filter = [...clientFiltersList, range]; + return filters; +}; + +type SupportedFields = 'locations' | 'ports' | 'schemes' | 'tags'; + +export const extractFilterAggsResults = ( + responseAggregations: Record, + keys: SupportedFields[] +): OverviewFilters => { + const values: OverviewFilters = { + locations: [], + ports: [], + schemes: [], + tags: [], + }; + keys.forEach(key => { + const buckets = responseAggregations[key]?.term?.buckets ?? []; + values[key] = buckets.map((item: { key: string | number }) => item.key); + }); + return values; +}; + +export const getFilterBar: UMElasticsearchQueryFn = async ({ + callES, + dateRangeStart, + dateRangeEnd, + search, + filterOptions, +}) => { + const aggs = generateFilterAggs( + [ + { aggName: 'locations', filterName: 'locations', field: 'observer.geo.name' }, + { aggName: 'ports', filterName: 'ports', field: 'url.port' }, + { aggName: 'schemes', filterName: 'schemes', field: 'monitor.type' }, + { aggName: 'tags', filterName: 'tags', field: 'tags' }, + ], + filterOptions + ); + const filters = combineRangeWithFilters(dateRangeStart, dateRangeEnd, search); + const params = { + index: INDEX_NAMES.HEARTBEAT, + body: { + size: 0, + query: { + ...filters, + }, + aggs, + }, + }; + + const { aggregations } = await callES('search', params); + return extractFilterAggsResults(aggregations, ['tags', 'locations', 'ports', 'schemes']); +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_index_pattern.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_index_pattern.ts new file mode 100644 index 0000000000000..4b40f800b6779 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_index_pattern.ts @@ -0,0 +1,45 @@ +/* + * 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 { APICaller } from 'src/core/server'; +import { UMElasticsearchQueryFn } from '../adapters'; +import { IndexPatternsFetcher, IIndexPattern } from '../../../../../../../src/plugins/data/server'; +import { INDEX_NAMES } from '../../../common/constants'; + +export const getUptimeIndexPattern: UMElasticsearchQueryFn = async callES => { + const indexPatternsFetcher = new IndexPatternsFetcher((...rest: Parameters) => + callES(...rest) + ); + + // Since `getDynamicIndexPattern` is called in setup_request (and thus by every endpoint) + // and since `getFieldsForWildcard` will throw if the specified indices don't exist, + // we have to catch errors here to avoid all endpoints returning 500 for users without APM data + // (would be a bad first time experience) + try { + const fields = await indexPatternsFetcher.getFieldsForWildcard({ + pattern: INDEX_NAMES.HEARTBEAT, + }); + + const indexPattern: IIndexPattern = { + fields, + title: INDEX_NAMES.HEARTBEAT, + }; + + return indexPattern; + } catch (e) { + const notExists = e.output?.statusCode === 404; + if (notExists) { + // eslint-disable-next-line no-console + console.error( + `Could not get dynamic index pattern because indices "${INDEX_NAMES.HEARTBEAT}" don't exist` + ); + return; + } + + // re-throw + throw e; + } +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_latest_monitor.ts new file mode 100644 index 0000000000000..bfaee3f2bf7ee --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -0,0 +1,78 @@ +/* + * 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 { UMElasticsearchQueryFn } from '../adapters'; +import { Ping } from '../../../common/graphql/types'; +import { INDEX_NAMES } from '../../../common/constants'; + +export interface GetLatestMonitorParams { + /** @member dateRangeStart timestamp bounds */ + dateStart: string; + + /** @member dateRangeEnd timestamp bounds */ + dateEnd: string; + + /** @member monitorId optional limit to monitorId */ + monitorId?: string | null; +} + +// Get The monitor latest state sorted by timestamp with date range +export const getLatestMonitor: UMElasticsearchQueryFn = async ({ + callES, + dateStart, + dateEnd, + monitorId, +}) => { + // TODO: Write tests for this function + + const params = { + index: INDEX_NAMES.HEARTBEAT, + body: { + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: dateStart, + lte: dateEnd, + }, + }, + }, + ...(monitorId ? [{ term: { 'monitor.id': monitorId } }] : []), + ], + }, + }, + size: 0, + aggs: { + by_id: { + terms: { + field: 'monitor.id', + size: 1000, + }, + aggs: { + latest: { + top_hits: { + size: 1, + sort: { + '@timestamp': { order: 'desc' }, + }, + }, + }, + }, + }, + }, + }, + }; + + const result = await callES('search', params); + const ping: any = result.aggregations.by_id.buckets?.[0]?.latest.hits?.hits?.[0] ?? {}; + + return { + ...ping?._source, + timestamp: ping?._source?.['@timestamp'], + }; +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor.ts new file mode 100644 index 0000000000000..94175616f374e --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor.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 { UMElasticsearchQueryFn } from '../adapters'; +import { Ping } from '../../../common/graphql/types'; +import { INDEX_NAMES } from '../../../common/constants'; + +export interface GetMonitorParams { + /** @member monitorId optional limit to monitorId */ + monitorId?: string | null; +} + +// Get the monitor meta info regardless of timestamp +export const getMonitor: UMElasticsearchQueryFn = async ({ + callES, + monitorId, +}) => { + const params = { + index: INDEX_NAMES.HEARTBEAT, + body: { + size: 1, + _source: ['url', 'monitor', 'observer'], + query: { + bool: { + filter: [ + { + term: { + 'monitor.id': monitorId, + }, + }, + ], + }, + }, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + }; + + const result = await callES('search', params); + + return result.hits.hits[0]?._source; +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_charts.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_charts.ts new file mode 100644 index 0000000000000..b97cc7287e921 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_charts.ts @@ -0,0 +1,176 @@ +/* + * 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 { UMElasticsearchQueryFn } from '../adapters'; +import { INDEX_NAMES } from '../../../common/constants'; +import { getHistogramIntervalFormatted } from '../helper'; +import { MonitorChart, LocationDurationLine } from '../../../common/graphql/types'; + +export interface GetMonitorChartsParams { + /** @member monitorId ID value for the selected monitor */ + monitorId: string; + /** @member dateRangeStart timestamp bounds */ + dateRangeStart: string; + /** @member dateRangeEnd timestamp bounds */ + dateRangeEnd: string; + /** @member location optional location value for use in filtering*/ + location?: string | null; +} + +const formatStatusBuckets = (time: any, buckets: any, docCount: any) => { + let up = null; + let down = null; + + buckets.forEach((bucket: any) => { + if (bucket.key === 'up') { + up = bucket.doc_count; + } else if (bucket.key === 'down') { + down = bucket.doc_count; + } + }); + + return { + x: time, + up, + down, + total: docCount, + }; +}; + +/** + * Fetches data used to populate monitor charts + */ +export const getMonitorCharts: UMElasticsearchQueryFn< + GetMonitorChartsParams, + MonitorChart +> = async ({ callES, dateRangeStart, dateRangeEnd, monitorId, location }) => { + const params = { + index: INDEX_NAMES.HEARTBEAT, + body: { + query: { + bool: { + filter: [ + { range: { '@timestamp': { gte: dateRangeStart, lte: dateRangeEnd } } }, + { term: { 'monitor.id': monitorId } }, + { term: { 'monitor.status': 'up' } }, + // if location is truthy, add it as a filter. otherwise add nothing + ...(!!location ? [{ term: { 'observer.geo.name': location } }] : []), + ], + }, + }, + size: 0, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: getHistogramIntervalFormatted(dateRangeStart, dateRangeEnd), + min_doc_count: 0, + }, + aggs: { + location: { + terms: { + field: 'observer.geo.name', + missing: 'N/A', + }, + aggs: { + status: { terms: { field: 'monitor.status', size: 2, shard_size: 2 } }, + duration: { stats: { field: 'monitor.duration.us' } }, + }, + }, + }, + }, + }, + }, + }; + + const result = await callES('search', params); + + const dateHistogramBuckets: any[] = result?.aggregations?.timeseries?.buckets ?? []; + + /** + * The code below is responsible for formatting the aggregation data we fetched above in a way + * that the chart components used by the client understands. + * There are five required values. Two are lists of points that conform to a simple (x,y) structure. + * + * The third list is for an area chart expressing a range, and it requires an (x,y,y0) structure, + * where y0 is the min value for the point and y is the max. + * + * Additionally, we supply the maximum value for duration and status, so the corresponding charts know + * what the domain size should be. + */ + const monitorChartsData: MonitorChart = { + locationDurationLines: [], + status: [], + durationMaxValue: 0, + statusMaxCount: 0, + }; + + /** + * The following section of code enables us to provide buckets per location + * that have a `null` value if there is no data at the given timestamp. + * + * We maintain two `Set`s. One is per bucket, the other is persisted for the + * entire collection. At the end of a bucket's evaluation, if there was no object + * parsed for a given location line that was already started, we insert an element + * to the given line with a null value. Without this, our charts on the client will + * display a continuous line for each of the points they are provided. + */ + + // a set of all the locations found for this result + const resultLocations = new Set(); + const linesByLocation: { [key: string]: LocationDurationLine } = {}; + dateHistogramBuckets.forEach(dateHistogramBucket => { + const x = dateHistogramBucket.key; + const docCount = dateHistogramBucket?.doc_count ?? 0; + // a set of all the locations for the current bucket + const bucketLocations = new Set(); + + dateHistogramBucket.location.buckets.forEach( + (locationBucket: { key: string; duration: { avg: number } }) => { + const locationName = locationBucket.key; + // store the location name in each set + bucketLocations.add(locationName); + resultLocations.add(locationName); + + // create a new line for this location if it doesn't exist + let currentLine: LocationDurationLine = linesByLocation?.[locationName] ?? undefined; + if (!currentLine) { + currentLine = { name: locationName, line: [] }; + linesByLocation[locationName] = currentLine; + monitorChartsData.locationDurationLines.push(currentLine); + } + // add the entry for the current location's duration average + currentLine.line.push({ x, y: locationBucket?.duration?.avg ?? null }); + } + ); + + // if there are more lines in the result than are represented in the current bucket, + // we must add null entries + if (dateHistogramBucket.location.buckets.length < resultLocations.size) { + resultLocations.forEach(resultLocation => { + // the current bucket has a value for this location, do nothing + if (location && location !== resultLocation) return; + // the current bucket had no value for this location, insert a null value + if (!bucketLocations.has(resultLocation)) { + const locationLine = monitorChartsData.locationDurationLines.find( + ({ name }) => name === resultLocation + ); + // in practice, there should always be a line present, but `find` can return `undefined` + if (locationLine) { + // this will create a gap in the line like we desire + locationLine.line.push({ x, y: null }); + } + } + }); + } + + monitorChartsData.status.push( + formatStatusBuckets(x, dateHistogramBucket?.status?.buckets ?? [], docCount) + ); + }); + + return monitorChartsData; +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_details.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_details.ts new file mode 100644 index 0000000000000..b516fde1ce844 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_details.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UMElasticsearchQueryFn } from '../adapters'; +import { MonitorDetails, MonitorError } from '../../../common/runtime_types'; +import { INDEX_NAMES } from '../../../common/constants'; + +export interface GetMonitorDetailsParams { + monitorId: string; + dateStart: string; + dateEnd: string; +} + +export const getMonitorDetails: UMElasticsearchQueryFn< + GetMonitorDetailsParams, + MonitorDetails +> = async ({ callES, monitorId, dateStart, dateEnd }) => { + const queryFilters: any = [ + { + range: { + '@timestamp': { + gte: dateStart, + lte: dateEnd, + }, + }, + }, + { + term: { + 'monitor.id': monitorId, + }, + }, + ]; + + const params = { + index: INDEX_NAMES.HEARTBEAT, + body: { + size: 1, + _source: ['error', '@timestamp'], + query: { + bool: { + must: [ + { + exists: { + field: 'error', + }, + }, + ], + filter: queryFilters, + }, + }, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + }; + + const result = await callES('search', params); + + const data = result.hits.hits[0]?._source; + + const monitorError: MonitorError | undefined = data?.error; + const errorTimeStamp: string | undefined = data?.['@timestamp']; + + return { + monitorId, + error: monitorError, + timestamp: errorTimeStamp, + }; +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_locations.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_locations.ts new file mode 100644 index 0000000000000..e1a0e14fe951d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_locations.ts @@ -0,0 +1,117 @@ +/* + * 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 { UMElasticsearchQueryFn } from '../adapters'; +import { INDEX_NAMES, UNNAMED_LOCATION } from '../../../common/constants'; +import { MonitorLocations, MonitorLocation } from '../../../common/runtime_types'; + +/** + * Fetch data for the monitor page title. + */ +export interface GetMonitorLocationsParams { + /** + * @member monitorId the ID to query + */ + monitorId: string; + dateStart: string; + dateEnd: string; +} + +export const getMonitorLocations: UMElasticsearchQueryFn< + GetMonitorLocationsParams, + MonitorLocations +> = async ({ callES, monitorId, dateStart, dateEnd }) => { + const params = { + index: INDEX_NAMES.HEARTBEAT, + body: { + size: 0, + query: { + bool: { + filter: [ + { + match: { + 'monitor.id': monitorId, + }, + }, + { + exists: { + field: 'summary', + }, + }, + { + range: { + '@timestamp': { + gte: dateStart, + lte: dateEnd, + }, + }, + }, + ], + }, + }, + aggs: { + location: { + terms: { + field: 'observer.geo.name', + missing: '__location_missing__', + }, + aggs: { + most_recent: { + top_hits: { + size: 1, + sort: { + '@timestamp': { + order: 'desc', + }, + }, + _source: ['monitor', 'summary', 'observer', '@timestamp'], + }, + }, + }, + }, + }, + }, + }; + + const result = await callES('search', params); + const locations = result?.aggregations?.location?.buckets ?? []; + + const getGeo = (locGeo: { name: string; location?: string }) => { + if (locGeo) { + const { name, location } = locGeo; + const latLon = location?.trim().split(','); + return { + name, + location: latLon + ? { + lat: latLon[0], + lon: latLon[1], + } + : undefined, + }; + } else { + return { + name: UNNAMED_LOCATION, + }; + } + }; + + const monLocs: MonitorLocation[] = []; + locations.forEach((loc: any) => { + const mostRecentLocation = loc.most_recent.hits.hits[0]._source; + const location: MonitorLocation = { + summary: mostRecentLocation?.summary, + geo: getGeo(mostRecentLocation?.observer?.geo), + timestamp: mostRecentLocation['@timestamp'], + }; + monLocs.push(location); + }); + + return { + monitorId, + locations: monLocs, + }; +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_states.ts new file mode 100644 index 0000000000000..32c82b1fa2098 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -0,0 +1,68 @@ +/* + * 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 { CONTEXT_DEFAULTS } from '../../../common/constants'; +import { fetchPage } from './search'; +import { UMElasticsearchQueryFn } from '../adapters'; +import { MonitorSummary, SortOrder, CursorDirection } from '../../../common/graphql/types'; +import { QueryContext } from './search'; + +export interface CursorPagination { + cursorKey?: any; + cursorDirection: CursorDirection; + sortOrder: SortOrder; +} + +export interface GetMonitorStatesParams { + dateRangeStart: string; + dateRangeEnd: string; + pagination?: CursorPagination; + filters?: string | null; + statusFilter?: string; +} + +export interface GetMonitorStatesResult { + summaries: MonitorSummary[]; + nextPagePagination: string | null; + prevPagePagination: string | null; +} + +// To simplify the handling of the group of pagination vars they're passed back to the client as a string +const jsonifyPagination = (p: any): string | null => { + if (!p) { + return null; + } + + return JSON.stringify(p); +}; + +// Gets a page of monitor states. +export const getMonitorStates: UMElasticsearchQueryFn< + GetMonitorStatesParams, + GetMonitorStatesResult +> = async ({ callES, dateRangeStart, dateRangeEnd, pagination, filters, statusFilter }) => { + pagination = pagination || CONTEXT_DEFAULTS.CURSOR_PAGINATION; + statusFilter = statusFilter === null ? undefined : statusFilter; + const size = 10; + + const queryContext = new QueryContext( + callES, + dateRangeStart, + dateRangeEnd, + pagination, + filters && filters !== '' ? JSON.parse(filters) : null, + size, + statusFilter + ); + + const page = await fetchPage(queryContext); + + return { + summaries: page.items, + nextPagePagination: jsonifyPagination(page.nextPagePagination), + prevPagePagination: jsonifyPagination(page.prevPagePagination), + }; +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/es_get_ping_historgram.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts similarity index 60% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/pings/es_get_ping_historgram.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts index 66cae497eb081..3b448dc31659b 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/es_get_ping_historgram.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -4,17 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; -import { INDEX_NAMES, QUERY } from '../../../../common/constants'; -import { parseFilterQuery, getFilterClause } from '../../helper'; -import { UMElasticsearchQueryFn } from '../framework'; -import { GetPingHistogramParams, HistogramResult } from '../../../../common/types'; +import { UMElasticsearchQueryFn } from '../adapters'; +import { parseFilterQuery, getFilterClause } from '../helper'; +import { INDEX_NAMES, QUERY } from '../../../common/constants'; import { HistogramQueryResult } from './types'; +import { HistogramResult } from '../../../common/types'; -export const esGetPingHistogram: UMElasticsearchQueryFn< +export interface GetPingHistogramParams { + /** @member dateRangeStart timestamp bounds */ + from: string; + /** @member dateRangeEnd timestamp bounds */ + to: string; + /** @member filters user-defined filters */ + filters?: string | null; + /** @member monitorId optional limit to monitorId */ + monitorId?: string | null; + /** @member statusFilter special filter targeting the latest status of each monitor */ + statusFilter?: string | null; +} + +export const getPingHistogram: UMElasticsearchQueryFn< GetPingHistogramParams, HistogramResult -> = async ({ callES, dateStart, dateEnd, filters, monitorId, statusFilter }) => { +> = async ({ callES, from, to, filters, monitorId, statusFilter }) => { const boolFilters = parseFilterQuery(filters); const additionalFilters = []; if (monitorId) { @@ -23,7 +35,7 @@ export const esGetPingHistogram: UMElasticsearchQueryFn< if (boolFilters) { additionalFilters.push(boolFilters); } - const filter = getFilterClause(dateStart, dateEnd, additionalFilters); + const filter = getFilterClause(from, to, additionalFilters); const params = { index: INDEX_NAMES.HEARTBEAT, @@ -63,11 +75,11 @@ export const esGetPingHistogram: UMElasticsearchQueryFn< const result = await callES('search', params); const interval = result.aggregations.timeseries?.interval; - const buckets: HistogramQueryResult[] = get(result, 'aggregations.timeseries.buckets', []); + const buckets: HistogramQueryResult[] = result?.aggregations?.timeseries?.buckets ?? []; const histogram = buckets.map(bucket => { - const x: number = get(bucket, 'key'); - const downCount: number = get(bucket, 'down.doc_count'); - const upCount: number = get(bucket, 'up.doc_count'); + const x: number = bucket.key; + const downCount: number = bucket.down.doc_count; + const upCount: number = bucket.up.doc_count; return { x, downCount: statusFilter && statusFilter !== 'down' ? 0 : downCount, diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_pings.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_pings.ts new file mode 100644 index 0000000000000..381aca720dc1d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_pings.ts @@ -0,0 +1,108 @@ +/* + * 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 { UMElasticsearchQueryFn } from '../adapters/framework'; +import { PingResults, Ping, HttpBody } from '../../../common/graphql/types'; +import { INDEX_NAMES } from '../../../common/constants'; + +export interface GetPingsParams { + /** @member dateRangeStart timestamp bounds */ + dateRangeStart: string; + + /** @member dateRangeEnd timestamp bounds */ + dateRangeEnd: string; + + /** @member monitorId optional limit by monitorId */ + monitorId?: string | null; + + /** @member status optional limit by check statuses */ + status?: string | null; + + /** @member sort optional sort by timestamp */ + sort?: string | null; + + /** @member size optional limit query size */ + size?: number | null; + + /** @member location optional location value for use in filtering*/ + location?: string | null; +} + +export const getPings: UMElasticsearchQueryFn = async ({ + callES, + dateRangeStart, + dateRangeEnd, + monitorId, + status, + sort, + size, + location, +}) => { + const sortParam = { sort: [{ '@timestamp': { order: sort ?? 'desc' } }] }; + const sizeParam = size ? { size } : undefined; + const filter: any[] = [{ range: { '@timestamp': { gte: dateRangeStart, lte: dateRangeEnd } } }]; + if (monitorId) { + filter.push({ term: { 'monitor.id': monitorId } }); + } + if (status) { + filter.push({ term: { 'monitor.status': status } }); + } + + let postFilterClause = {}; + if (location) { + postFilterClause = { post_filter: { term: { 'observer.geo.name': location } } }; + } + const queryContext = { bool: { filter } }; + const params = { + index: INDEX_NAMES.HEARTBEAT, + body: { + query: { + ...queryContext, + }, + ...sortParam, + ...sizeParam, + aggregations: { + locations: { + terms: { + field: 'observer.geo.name', + missing: 'N/A', + size: 1000, + }, + }, + }, + ...postFilterClause, + }, + }; + + const { + hits: { hits, total }, + aggregations: aggs, + } = await callES('search', params); + + const locations = aggs?.locations ?? { buckets: [{ key: 'N/A', doc_count: 0 }] }; + + const pings: Ping[] = hits.map(({ _id, _source }: any) => { + const timestamp = _source['@timestamp']; + + // Calculate here the length of the content string in bytes, this is easier than in client JS, where + // we don't have access to Buffer.byteLength. There are some hacky ways to do this in the + // client but this is cleaner. + const httpBody: HttpBody | undefined = _source?.http?.response?.body; + if (httpBody && httpBody.content) { + httpBody.content_bytes = Buffer.byteLength(httpBody.content); + } + + return { id: _id, timestamp, ..._source }; + }); + + const results: PingResults = { + total: total.value, + locations: locations.buckets.map((bucket: { key: string }) => bucket.key), + pings, + }; + + return results; +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts new file mode 100644 index 0000000000000..6236971146015 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -0,0 +1,102 @@ +/* + * 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 { UMElasticsearchQueryFn } from '../adapters'; +import { Snapshot } from '../../../common/runtime_types'; +import { QueryContext, MonitorGroupIterator } from './search'; +import { CONTEXT_DEFAULTS, INDEX_NAMES } from '../../../common/constants'; + +export interface GetSnapshotCountParams { + dateRangeStart: string; + dateRangeEnd: string; + filters?: string | null; + statusFilter?: string; +} + +const fastStatusCount = async (context: QueryContext): Promise => { + const params = { + index: INDEX_NAMES.HEARTBEAT, + body: { + size: 0, + query: { bool: { filter: await context.dateAndCustomFilters() } }, + aggs: { + unique: { + // We set the precision threshold to 40k which is the max precision supported by cardinality + cardinality: { field: 'monitor.id', precision_threshold: 40000 }, + }, + down: { + filter: { range: { 'summary.down': { gt: 0 } } }, + aggs: { + unique: { cardinality: { field: 'monitor.id', precision_threshold: 40000 } }, + }, + }, + }, + }, + }; + + const statistics = await context.search(params); + const total = statistics.aggregations.unique.value; + const down = statistics.aggregations.down.unique.value; + + return { + total, + down, + up: total - down, + }; +}; + +const slowStatusCount = async (context: QueryContext, status: string): Promise => { + const downContext = context.clone(); + downContext.statusFilter = status; + const iterator = new MonitorGroupIterator(downContext); + let count = 0; + while (await iterator.next()) { + count++; + } + return count; +}; + +export const getSnapshotCount: UMElasticsearchQueryFn = async ({ + callES, + dateRangeStart, + dateRangeEnd, + filters, + statusFilter, +}): Promise => { + if (!(statusFilter === 'up' || statusFilter === 'down' || statusFilter === undefined)) { + throw new Error(`Invalid status filter value '${statusFilter}'`); + } + + const context = new QueryContext( + callES, + dateRangeStart, + dateRangeEnd, + CONTEXT_DEFAULTS.CURSOR_PAGINATION, + filters && filters !== '' ? JSON.parse(filters) : null, + Infinity, + statusFilter + ); + + // Calculate the total, up, and down counts. + const counts = await fastStatusCount(context); + + // Check if the last count was accurate, if not, we need to perform a slower count with the + // MonitorGroupsIterator. + if (!(await context.hasTimespan())) { + // Figure out whether 'up' or 'down' is more common. It's faster to count the lower cardinality + // one then use subtraction to figure out its opposite. + const [leastCommonStatus, mostCommonStatus]: Array<'up' | 'down'> = + counts.up > counts.down ? ['down', 'up'] : ['up', 'down']; + counts[leastCommonStatus] = await slowStatusCount(context, leastCommonStatus); + counts[mostCommonStatus] = counts.total - counts[leastCommonStatus]; + } + + return { + total: statusFilter ? counts[statusFilter] : counts.total, + up: statusFilter === 'down' ? 0 : counts.up, + down: statusFilter === 'up' ? 0 : counts.down, + }; +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/get_states_index_status.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/get_states_index_status.ts new file mode 100644 index 0000000000000..5044b9a6932cf --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/get_states_index_status.ts @@ -0,0 +1,24 @@ +/* + * 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 { UMElasticsearchQueryFn } from '../adapters'; +import { StatesIndexStatus } from '../../../common/graphql/types'; +import { INDEX_NAMES } from '../../../common/constants'; + +export const getStatesIndexStatus: UMElasticsearchQueryFn<{}, StatesIndexStatus> = async ({ + callES, +}) => { + const { + _shards: { total }, + count, + } = await callES('count', { index: INDEX_NAMES.HEARTBEAT }); + return { + indexExists: total > 0, + docCount: { + count, + }, + }; +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/index.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/index.ts new file mode 100644 index 0000000000000..f41b7257524fd --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { getDocCount } from './get_doc_count'; +export { getFilterBar, GetFilterBarParams } from './get_filter_bar'; +export { getUptimeIndexPattern as getIndexPattern } from './get_index_pattern'; +export { getLatestMonitor, GetLatestMonitorParams } from './get_latest_monitor'; +export { getMonitor, GetMonitorParams } from './get_monitor'; +export { getMonitorCharts, GetMonitorChartsParams } from './get_monitor_charts'; +export { getMonitorDetails, GetMonitorDetailsParams } from './get_monitor_details'; +export { getMonitorLocations, GetMonitorLocationsParams } from './get_monitor_locations'; +export { getMonitorStates, GetMonitorStatesParams } from './get_monitor_states'; +export { getPings, GetPingsParams } from './get_pings'; +export { getPingHistogram, GetPingHistogramParams } from './get_ping_histogram'; +export { UptimeRequests } from './uptime_requests'; +export { getSnapshotCount, GetSnapshotCountParams } from './get_snapshot_counts'; +export { getStatesIndexStatus } from './get_states_index_status'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/fetch_page.test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts similarity index 97% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/fetch_page.test.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts index 0bbdaa87a5e66..d519a4e75463f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/fetch_page.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts @@ -12,7 +12,7 @@ import { MonitorGroupsPage, } from '../fetch_page'; import { QueryContext } from '../query_context'; -import { MonitorSummary } from '../../../../../../common/graphql/types'; +import { MonitorSummary } from '../../../../../common/graphql/types'; import { nextPagination, prevPagination, simpleQueryContext } from './test_helpers'; const simpleFixture: MonitorGroups[] = [ diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/find_potential_matches_test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/find_potential_matches_test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/find_potential_matches_test.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/find_potential_matches_test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/monitor_group_iterator.test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/monitor_group_iterator.test.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts similarity index 93% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts index 8924d07ac0c4d..d02d23276f220 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts @@ -5,10 +5,10 @@ */ import { QueryContext } from '../query_context'; -import { CursorPagination } from '../..'; -import { CursorDirection, SortOrder } from '../../../../../../common/graphql/types'; +import { CursorPagination } from '../types'; +import { CursorDirection, SortOrder } from '../../../../../common/graphql/types'; -describe(QueryContext, () => { +describe('QueryContext', () => { // 10 minute range const rangeStart = '2019-02-03T19:06:54.939Z'; const rangeEnd = '2019-02-03T19:16:54.939Z'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/test_helpers.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts similarity index 85% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/test_helpers.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts index bb3f3da3e289d..98b192d14f91a 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/test_helpers.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CursorPagination } from '../../adapter_types'; -import { CursorDirection, SortOrder } from '../../../../../../common/graphql/types'; +import { CursorPagination } from '../types'; +import { CursorDirection, SortOrder } from '../../../../../common/graphql/types'; import { QueryContext } from '../query_context'; export const prevPagination = (key: any): CursorPagination => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/enrich_monitor_groups.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts similarity index 98% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/enrich_monitor_groups.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts index b64015424ff40..e37c749e63566 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/enrich_monitor_groups.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts @@ -6,15 +6,15 @@ import { get, sortBy } from 'lodash'; import { QueryContext } from './query_context'; -import { getHistogramIntervalFormatted } from '../../../helper'; -import { INDEX_NAMES, STATES } from '../../../../../common/constants'; +import { getHistogramIntervalFormatted } from '../../helper'; +import { INDEX_NAMES, STATES } from '../../../../common/constants'; import { MonitorSummary, SummaryHistogram, Check, CursorDirection, SortOrder, -} from '../../../../../common/graphql/types'; +} from '../../../../common/graphql/types'; import { MonitorEnricher } from './fetch_page'; export const enrichMonitorGroups: MonitorEnricher = async ( diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_chunk.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_chunk.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_chunk.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_chunk.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_page.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_page.ts similarity index 97% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_page.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_page.ts index 046bdc8a8d07d..6440850dc0ffc 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/fetch_page.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/search/fetch_page.ts @@ -5,10 +5,10 @@ */ import { flatten } from 'lodash'; -import { CursorPagination } from '../adapter_types'; +import { CursorPagination } from './types'; import { QueryContext } from './query_context'; -import { QUERY } from '../../../../../common/constants'; -import { CursorDirection, MonitorSummary, SortOrder } from '../../../../../common/graphql/types'; +import { QUERY } from '../../../../common/constants'; +import { CursorDirection, MonitorSummary, SortOrder } from '../../../../common/graphql/types'; import { enrichMonitorGroups } from './enrich_monitor_groups'; import { MonitorGroupIterator } from './monitor_group_iterator'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/find_potential_matches.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index 634d6369531d8..fc0e35b279e0b 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/find_potential_matches.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -5,8 +5,8 @@ */ import { get, set } from 'lodash'; -import { CursorDirection } from '../../../../../common/graphql/types'; -import { INDEX_NAMES } from '../../../../../common/constants'; +import { CursorDirection } from '../../../../common/graphql/types'; +import { INDEX_NAMES } from '../../../../common/constants'; import { QueryContext } from './query_context'; // This is the first phase of the query. In it, we find the most recent check groups that matched the given query. diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/index.ts similarity index 89% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/index.ts index 040c256935692..8de5687808d61 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/index.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/search/index.ts @@ -6,3 +6,4 @@ export { fetchPage, MonitorGroups, MonitorLocCheckGroup, MonitorGroupsPage } from './fetch_page'; export { MonitorGroupIterator } from './monitor_group_iterator'; +export { QueryContext } from './query_context'; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts similarity index 98% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts index 27c16863a37ab..ced557dbf62e0 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/monitor_group_iterator.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts @@ -5,10 +5,10 @@ */ import { QueryContext } from './query_context'; -import { CursorPagination } from '../adapter_types'; import { fetchChunk } from './fetch_chunk'; -import { CursorDirection } from '../../../../../common/graphql/types'; +import { CursorDirection } from '../../../../common/graphql/types'; import { MonitorGroups } from './fetch_page'; +import { CursorPagination } from './types'; // Hardcoded chunk size for how many monitors to fetch at a time when querying export const CHUNK_SIZE = 1000; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/query_context.ts similarity index 94% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/query_context.ts index a51931ba11630..f5b13c165d87d 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/search/query_context.ts @@ -5,10 +5,10 @@ */ import moment from 'moment'; -import { APICaller } from 'kibana/server'; -import { CursorPagination } from '../adapter_types'; -import { INDEX_NAMES } from '../../../../../common/constants'; -import { parseRelativeDate } from '../../../helper/get_histogram_interval'; +import { APICaller } from 'src/core/server'; +import { INDEX_NAMES } from '../../../../common/constants'; +import { CursorPagination } from './types'; +import { parseRelativeDate } from '../../helper'; export class QueryContext { callES: APICaller; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/refine_potential_matches.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts similarity index 97% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/refine_potential_matches.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index f8347d0737521..a55301555c8bf 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/refine_potential_matches.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { INDEX_NAMES } from '../../../../../common/constants'; +import { INDEX_NAMES } from '../../../../common/constants'; import { QueryContext } from './query_context'; -import { CursorDirection } from '../../../../../common/graphql/types'; +import { CursorDirection } from '../../../../common/graphql/types'; import { MonitorGroups, MonitorLocCheckGroup } from './fetch_page'; /** diff --git a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/index.js b/x-pack/legacy/plugins/uptime/server/lib/requests/search/types.ts similarity index 55% rename from x-pack/legacy/plugins/rollup/server/lib/error_wrappers/index.js rename to x-pack/legacy/plugins/uptime/server/lib/requests/search/types.ts index f275f15637091..dc6021a91146a 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/error_wrappers/index.js +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/search/types.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export { wrapCustomError } from './wrap_custom_error'; -export { wrapEsError } from './wrap_es_error'; -export { wrapUnknownError } from './wrap_unknown_error'; +import { CursorDirection, SortOrder } from '../../../../common/graphql/types'; + +export interface CursorPagination { + cursorKey?: any; + cursorDirection: CursorDirection; + sortOrder: SortOrder; +} diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/types.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/types.ts similarity index 88% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/pings/types.ts rename to x-pack/legacy/plugins/uptime/server/lib/requests/types.ts index b1b1589af2ca7..7f65e80113d8f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/types.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DocCount, Ping, PingResults } from '../../../../common/graphql/types'; -import { UMElasticsearchQueryFn } from '../framework'; -import { GetPingHistogramParams, HistogramResult } from '../../../../common/types'; +import { DocCount, Ping, PingResults } from '../../../common/graphql/types'; +import { UMElasticsearchQueryFn } from '../adapters'; +import { GetPingHistogramParams, HistogramResult } from '../../../common/types'; export interface GetAllParams { /** @member dateRangeStart timestamp bounds */ @@ -63,13 +63,12 @@ export interface UMPingsAdapter { export interface HistogramQueryResult { key: number; + key_as_string: string; doc_count: number; - bucket_total: { - value: number; - }; down: { - bucket_count: { - value: number; - }; + doc_count: number; + }; + up: { + doc_count: number; }; } diff --git a/x-pack/legacy/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/legacy/plugins/uptime/server/lib/requests/uptime_requests.ts new file mode 100644 index 0000000000000..182c944e8388a --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/requests/uptime_requests.ts @@ -0,0 +1,52 @@ +/* + * 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 { UMElasticsearchQueryFn } from '../adapters'; +import { + DocCount, + Ping, + MonitorChart, + PingResults, + StatesIndexStatus, +} from '../../../common/graphql/types'; +import { + GetFilterBarParams, + GetLatestMonitorParams, + GetMonitorParams, + GetMonitorChartsParams, + GetMonitorDetailsParams, + GetMonitorLocationsParams, + GetMonitorStatesParams, + GetPingsParams, + GetPingHistogramParams, +} from '.'; +import { + OverviewFilters, + MonitorDetails, + MonitorLocations, + Snapshot, +} from '../../../common/runtime_types'; +import { GetMonitorStatesResult } from './get_monitor_states'; +import { GetSnapshotCountParams } from './get_snapshot_counts'; +import { HistogramResult } from '../../../common/types'; + +type ESQ = UMElasticsearchQueryFn; + +export interface UptimeRequests { + getDocCount: ESQ<{}, DocCount>; + getFilterBar: ESQ; + getIndexPattern: ESQ; + getLatestMonitor: ESQ; + getMonitor: ESQ; + getMonitorCharts: ESQ; + getMonitorDetails: ESQ; + getMonitorLocations: ESQ; + getMonitorStates: ESQ; + getPings: ESQ; + getPingHistogram: ESQ; + getSnapshotCount: ESQ; + getStatesIndexStatus: ESQ<{}, StatesIndexStatus>; +} diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts index 91936b499d8e6..aa3b36ec7d919 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts @@ -5,7 +5,7 @@ */ import { createGetOverviewFilters } from './overview_filters'; -import { createGetAllRoute } from './pings'; +import { createGetPingsRoute } from './pings'; import { createGetIndexPatternRoute } from './index_pattern'; import { createLogMonitorPageRoute, createLogOverviewPageRoute } from './telemetry'; import { createGetSnapshotCount } from './snapshot'; @@ -23,7 +23,7 @@ export { createRouteWithAuth } from './create_route_with_auth'; export { uptimeRouteWrapper } from './uptime_route_wrapper'; export const restApiRoutes: UMRestApiRouteFactory[] = [ createGetOverviewFilters, - createGetAllRoute, + createGetPingsRoute, createGetIndexPatternRoute, createGetMonitorRoute, createGetMonitorDetailsRoute, diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts b/x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts index cee8eaf3f9cae..cc65749153c1d 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/index_pattern/get_index_pattern.ts @@ -18,7 +18,7 @@ export const createGetIndexPatternRoute: UMRestApiRouteFactory = (libs: UMServer try { return response.ok({ body: { - ...(await libs.stubIndexPattern.getUptimeIndexPattern(callES)), + ...(await libs.requests.getIndexPattern(callES)), }, }); } catch (e) { diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitor_locations.ts index b2356ae0a88bf..f8c7666f53f7d 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitor_locations.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitor_locations.ts @@ -26,7 +26,7 @@ export const createGetMonitorLocationsRoute: UMRestApiRouteFactory = (libs: UMSe return response.ok({ body: { - ...(await libs.monitors.getMonitorLocations({ + ...(await libs.requests.getMonitorLocations({ callES, monitorId, dateStart, diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts index 9e1bc6f0d6a96..ca88dd965c1ad 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -25,7 +25,7 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ const { monitorId, dateStart, dateEnd } = request.query; return response.ok({ body: { - ...(await libs.monitors.getMonitorDetails({ + ...(await libs.requests.getMonitorDetails({ callES, monitorId, dateStart, diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/status.ts b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/status.ts index 8b1bc04b45110..8dac50c9f5905 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/status.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/status.ts @@ -24,7 +24,7 @@ export const createGetMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) return response.ok({ body: { - ...(await libs.pings.getMonitor({ callES, monitorId })), + ...(await libs.requests.getMonitor({ callES, monitorId })), }, }); }, @@ -45,7 +45,7 @@ export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLib }, handler: async ({ callES }, _context, request, response): Promise => { const { monitorId, dateStart, dateEnd } = request.query; - const result = await libs.pings.getLatestMonitorStatus({ + const result = await libs.requests.getLatestMonitor({ callES, monitorId, dateStart, diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts b/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts index ef93253bb5b70..02e54cb441838 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts @@ -43,7 +43,7 @@ export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLi } } - const filtersResponse = await libs.monitors.getFilterBar({ + const filtersResponse = await libs.requests.getFilterBar({ callES, dateRangeStart, dateRangeEnd, diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts index c8eb2a1e40ad4..93ba4490fa31f 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts @@ -26,10 +26,10 @@ export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServe handler: async ({ callES }, _context, request, response): Promise => { const { dateStart, dateEnd, statusFilter, monitorId, filters } = request.query; - const result = await libs.pings.getPingHistogram({ + const result = await libs.requests.getPingHistogram({ callES, - dateStart, - dateEnd, + from: dateStart, + to: dateEnd, monitorId, statusFilter, filters, diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_all.ts b/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_pings.ts similarity index 90% rename from x-pack/legacy/plugins/uptime/server/rest_api/pings/get_all.ts rename to x-pack/legacy/plugins/uptime/server/rest_api/pings/get_pings.ts index 824035954ea28..e57951c98b6fc 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_all.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/pings/get_pings.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -export const createGetAllRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ +export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', path: '/api/uptime/pings', validate: { @@ -28,7 +28,7 @@ export const createGetAllRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => handler: async ({ callES }, _context, request, response): Promise => { const { dateRangeStart, dateRangeEnd, location, monitorId, size, sort, status } = request.query; - const result = await libs.pings.getAll({ + const result = await libs.requests.getPings({ callES, dateRangeStart, dateRangeEnd, diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/pings/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/pings/index.ts index 43acf5410c8ed..abb7da26f994f 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/pings/index.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/pings/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createGetAllRoute } from './get_all'; +export { createGetPingsRoute } from './get_pings'; diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts index 986ac797d63b6..c51806e323307 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -24,7 +24,7 @@ export const createGetSnapshotCount: UMRestApiRouteFactory = (libs: UMServerLibs }, handler: async ({ callES }, _context, request, response): Promise => { const { dateRangeStart, dateRangeEnd, filters, statusFilter } = request.query; - const result = await libs.monitorStates.getSnapshotCount({ + const result = await libs.requests.getSnapshotCount({ callES, dateRangeStart, dateRangeEnd, diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/types.ts b/x-pack/legacy/plugins/uptime/server/rest_api/types.ts index e0c8ba4a286cf..a0566c225eae7 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/types.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/types.ts @@ -15,7 +15,7 @@ import { KibanaRequest, KibanaResponseFactory, IKibanaResponse, -} from 'kibana/server'; +} from 'src/core/server'; import { UMServerLibs } from '../lib/lib'; /** diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx index ca11fe91abdbf..aa31b035cda58 100644 --- a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { IEmbeddable, Embeddable, EmbeddableInput } from 'src/plugins/embeddable/public'; -import { IAction, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { Action, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; import { TimeRange } from '../../../../src/plugins/data/public'; import { CustomizeTimeRangeModal } from './customize_time_range_modal'; import { OpenModal, CommonlyUsedRange } from './types'; @@ -38,7 +38,7 @@ interface ActionContext { embeddable: Embeddable; } -export class CustomTimeRangeAction implements IAction { +export class CustomTimeRangeAction implements Action { public readonly type = CUSTOM_TIME_RANGE; private openModal: OpenModal; private dateFormat?: string; diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.tsx b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.tsx index 78fe8e01e599e..4ee8c91ff2a32 100644 --- a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { prettyDuration, commonDurationRanges } from '@elastic/eui'; import { IEmbeddable, Embeddable, EmbeddableInput } from 'src/plugins/embeddable/public'; -import { IAction, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { Action, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; import { TimeRange } from '../../../../src/plugins/data/public'; import { CustomizeTimeRangeModal } from './customize_time_range_modal'; import { doesInheritTimeRange } from './does_inherit_time_range'; @@ -29,7 +29,7 @@ interface ActionContext { embeddable: Embeddable; } -export class CustomTimeRangeBadge implements IAction { +export class CustomTimeRangeBadge implements Action { public readonly type = CUSTOM_TIME_RANGE_BADGE; public readonly id = CUSTOM_TIME_RANGE_BADGE; public order = 7; diff --git a/x-pack/plugins/advanced_ui_actions/public/plugin.ts b/x-pack/plugins/advanced_ui_actions/public/plugin.ts index cc4a7c90de513..5c5d2d38da15e 100644 --- a/x-pack/plugins/advanced_ui_actions/public/plugin.ts +++ b/x-pack/plugins/advanced_ui_actions/public/plugin.ts @@ -11,7 +11,7 @@ import { Plugin, } from '../../../../src/core/public'; import { createReactOverlays } from '../../../../src/plugins/kibana_react/public'; -import { IUiActionsStart, IUiActionsSetup } from '../../../../src/plugins/ui_actions/public'; +import { UiActionsStart, UiActionsSetup } from '../../../../src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, @@ -25,12 +25,12 @@ import { CommonlyUsedRange } from './types'; interface SetupDependencies { embeddable: IEmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. - uiActions: IUiActionsSetup; + uiActions: UiActionsSetup; } interface StartDependencies { embeddable: IEmbeddableStart; - uiActions: IUiActionsStart; + uiActions: UiActionsStart; } export type Setup = void; diff --git a/x-pack/plugins/features/common/feature.ts b/x-pack/plugins/features/common/feature.ts index 423fe1eb99704..748076b95ad77 100644 --- a/x-pack/plugins/features/common/feature.ts +++ b/x-pack/plugins/features/common/feature.ts @@ -43,7 +43,7 @@ export interface Feature< * This does not restrict access to your feature based on license. * Its only purpose is to inform the space and roles UIs on which features to display. */ - validLicenses?: Array<'basic' | 'standard' | 'gold' | 'platinum' | 'enterprise'>; + validLicenses?: Array<'basic' | 'standard' | 'gold' | 'platinum' | 'enterprise' | 'trial'>; /** * An optional EUI Icon to be used when displaying your feature. diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 7732686db5ee1..cc12ea1b78dce 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -51,7 +51,7 @@ const schema = Joi.object({ name: Joi.string().required(), excludeFromBasePrivileges: Joi.boolean(), validLicenses: Joi.array().items( - Joi.string().valid('basic', 'standard', 'gold', 'platinum', 'enterprise') + Joi.string().valid('basic', 'standard', 'gold', 'platinum', 'enterprise', 'trial') ), icon: Joi.string(), description: Joi.string(), diff --git a/x-pack/plugins/features/server/routes/index.test.ts b/x-pack/plugins/features/server/routes/index.test.ts index 98a23a61d542c..b0f8417b7175d 100644 --- a/x-pack/plugins/features/server/routes/index.test.ts +++ b/x-pack/plugins/features/server/routes/index.test.ts @@ -53,7 +53,7 @@ describe('GET /api/features', () => { it('returns a list of available features', async () => { const mockResponse = httpServerMock.createResponseFactory(); - routeHandler(undefined as any, undefined as any, mockResponse); + routeHandler(undefined as any, { query: {} } as any, mockResponse); expect(mockResponse.ok.mock.calls).toMatchInlineSnapshot(` Array [ @@ -84,11 +84,11 @@ describe('GET /api/features', () => { `); }); - it(`does not return features that arent allowed by current license`, async () => { + it(`by default does not return features that arent allowed by current license`, async () => { currentLicenseLevel = 'basic'; const mockResponse = httpServerMock.createResponseFactory(); - routeHandler(undefined as any, undefined as any, mockResponse); + routeHandler(undefined as any, { query: {} } as any, mockResponse); expect(mockResponse.ok.mock.calls).toMatchInlineSnapshot(` Array [ @@ -107,4 +107,63 @@ describe('GET /api/features', () => { ] `); }); + + it(`ignoreValidLicenses=false does not return features that arent allowed by current license`, async () => { + currentLicenseLevel = 'basic'; + + const mockResponse = httpServerMock.createResponseFactory(); + routeHandler(undefined as any, { query: { ignoreValidLicenses: false } } as any, mockResponse); + + expect(mockResponse.ok.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "body": Array [ + Object { + "app": Array [], + "id": "feature_1", + "name": "Feature 1", + "privileges": Object {}, + }, + ], + }, + ], + ] + `); + }); + + it(`ignoreValidLicenses=true returns features that arent allowed by current license`, async () => { + currentLicenseLevel = 'basic'; + + const mockResponse = httpServerMock.createResponseFactory(); + routeHandler(undefined as any, { query: { ignoreValidLicenses: true } } as any, mockResponse); + + expect(mockResponse.ok.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "body": Array [ + Object { + "app": Array [], + "id": "feature_1", + "name": "Feature 1", + "privileges": Object {}, + }, + Object { + "app": Array [ + "bar-app", + ], + "id": "licensed_feature", + "name": "Licensed Feature", + "privileges": Object {}, + "validLicenses": Array [ + "gold", + ], + }, + ], + }, + ], + ] + `); + }); }); diff --git a/x-pack/plugins/features/server/routes/index.ts b/x-pack/plugins/features/server/routes/index.ts index 51869c39cf83c..cf4d61ccac88b 100644 --- a/x-pack/plugins/features/server/routes/index.ts +++ b/x-pack/plugins/features/server/routes/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { schema } from '@kbn/config-schema'; import { IRouter } from '../../../../../src/core/server'; import { LegacyAPI } from '../plugin'; import { FeatureRegistry } from '../feature_registry'; @@ -19,13 +20,20 @@ export interface RouteDefinitionParams { export function defineRoutes({ router, featureRegistry, getLegacyAPI }: RouteDefinitionParams) { router.get( - { path: '/api/features', options: { tags: ['access:features'] }, validate: false }, + { + path: '/api/features', + options: { tags: ['access:features'] }, + validate: { + query: schema.object({ ignoreValidLicenses: schema.boolean({ defaultValue: false }) }), + }, + }, (context, request, response) => { const allFeatures = featureRegistry.getAll(); return response.ok({ body: allFeatures.filter( feature => + request.query.ignoreValidLicenses || !feature.validLicenses || !feature.validLicenses.length || getLegacyAPI().xpackInfo.license.isOneOf(feature.validLicenses) diff --git a/x-pack/plugins/oss_telemetry/server/test_utils/index.ts b/x-pack/plugins/oss_telemetry/server/test_utils/index.ts index fc8c98c164623..0f0f6a36d2ed3 100644 --- a/x-pack/plugins/oss_telemetry/server/test_utils/index.ts +++ b/x-pack/plugins/oss_telemetry/server/test_utils/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { APICaller, CoreSetup } from 'kibana/server'; +import { APICaller, CoreSetup } from 'src/core/server'; import { of } from 'rxjs'; import { 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/legacy/plugins/uptime/server/lib/adapters/pings/index.ts b/x-pack/plugins/rollup/server/index.ts similarity index 50% rename from x-pack/legacy/plugins/uptime/server/lib/adapters/pings/index.ts rename to x-pack/plugins/rollup/server/index.ts index 37324a8f521f6..4056842453776 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/index.ts +++ b/x-pack/plugins/rollup/server/index.ts @@ -4,5 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './types'; -export { elasticsearchPingsAdapter } from './es_pings'; +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/task_manager/server/README.md b/x-pack/plugins/task_manager/server/README.md index a067358dc8841..a4154f3ecf212 100644 --- a/x-pack/plugins/task_manager/server/README.md +++ b/x-pack/plugins/task_manager/server/README.md @@ -261,6 +261,9 @@ The _Start_ Plugin api allow you to use Task Manager to facilitate your Plugin's remove: (id: string) => { // ... }, + get: (id: string) => { + // ... + }, schedule: (taskInstance: TaskInstanceWithDeprecatedFields, options?: any) => { // ... }, diff --git a/x-pack/plugins/task_manager/server/create_task_manager.test.ts b/x-pack/plugins/task_manager/server/create_task_manager.test.ts index 34258e15f45d1..133cfcac4c046 100644 --- a/x-pack/plugins/task_manager/server/create_task_manager.test.ts +++ b/x-pack/plugins/task_manager/server/create_task_manager.test.ts @@ -42,20 +42,21 @@ describe('createTaskManager', () => { const mockLegacyDeps = getMockLegacyDeps(); const setupResult = createTaskManager(mockCoreSetup, mockLegacyDeps); expect(setupResult).toMatchInlineSnapshot(` - TaskManager { - "addMiddleware": [MockFunction], - "assertUninitialized": [MockFunction], - "attemptToRun": [MockFunction], - "ensureScheduled": [MockFunction], - "fetch": [MockFunction], - "registerTaskDefinitions": [MockFunction], - "remove": [MockFunction], - "runNow": [MockFunction], - "schedule": [MockFunction], - "start": [MockFunction], - "stop": [MockFunction], - "waitUntilStarted": [MockFunction], - } - `); + TaskManager { + "addMiddleware": [MockFunction], + "assertUninitialized": [MockFunction], + "attemptToRun": [MockFunction], + "ensureScheduled": [MockFunction], + "fetch": [MockFunction], + "get": [MockFunction], + "registerTaskDefinitions": [MockFunction], + "remove": [MockFunction], + "runNow": [MockFunction], + "schedule": [MockFunction], + "start": [MockFunction], + "stop": [MockFunction], + "waitUntilStarted": [MockFunction], + } + `); }); }); diff --git a/x-pack/plugins/task_manager/server/mocks.ts b/x-pack/plugins/task_manager/server/mocks.ts index 00b27bd55e7dd..8ec05dd1bd401 100644 --- a/x-pack/plugins/task_manager/server/mocks.ts +++ b/x-pack/plugins/task_manager/server/mocks.ts @@ -18,6 +18,7 @@ const createSetupMock = () => { const createStartMock = () => { const mock: jest.Mocked = { fetch: jest.fn(), + get: jest.fn(), remove: jest.fn(), schedule: jest.fn(), runNow: jest.fn(), diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 5e59be65c729d..fdfe0c068afcf 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -21,7 +21,7 @@ export type TaskManagerSetupContract = { export type TaskManagerStartContract = Pick< TaskManager, - 'fetch' | 'remove' | 'schedule' | 'runNow' | 'ensureScheduled' + 'fetch' | 'get' | 'remove' | 'schedule' | 'runNow' | 'ensureScheduled' >; export class TaskManagerPlugin @@ -69,6 +69,7 @@ export class TaskManagerPlugin public start(): TaskManagerStartContract { return { fetch: (...args) => this.taskManager.then(tm => tm.fetch(...args)), + get: (...args) => this.taskManager.then(tm => tm.get(...args)), remove: (...args) => this.taskManager.then(tm => tm.remove(...args)), schedule: (...args) => this.taskManager.then(tm => tm.schedule(...args)), runNow: (...args) => this.taskManager.then(tm => tm.runNow(...args)), diff --git a/x-pack/plugins/task_manager/server/task_manager.mock.ts b/x-pack/plugins/task_manager/server/task_manager.mock.ts index 89d1210b00671..1be1a81cdeb68 100644 --- a/x-pack/plugins/task_manager/server/task_manager.mock.ts +++ b/x-pack/plugins/task_manager/server/task_manager.mock.ts @@ -21,6 +21,7 @@ export const taskManagerMock = { ensureScheduled: jest.fn(), schedule: jest.fn(), fetch: jest.fn(), + get: jest.fn(), runNow: jest.fn(), remove: jest.fn(), ...overrides, diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index da9640fa3e071..641826de615b1 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -333,6 +333,17 @@ export class TaskManager { return this.store.fetch(opts); } + /** + * Get the current state of a specified task. + * + * @param {string} id + * @returns {Promise} + */ + public async get(id: string): Promise { + await this.waitUntilStarted(); + return this.store.get(id); + } + /** * Removes the specified task from the index. * diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ded018711e2af..6af1fd25df3f8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10254,9 +10254,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 9330b597047bd..74314aa588a9c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10253,9 +10253,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/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index 6c2a22f2737fe..f7f3d0fa91fff 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/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 { times } from 'lodash'; import { schema } from '@kbn/config-schema'; import { AlertExecutorOptions, AlertType } from '../../../../../../legacy/plugins/alerting'; import { ActionTypeExecutorOptions, ActionType } from '../../../../../../plugins/actions/server'; @@ -249,6 +249,29 @@ export default function(kibana: any) { }; }, }; + // Alert types + const cumulativeFiringAlertType: AlertType = { + id: 'test.cumulative-firing', + name: 'Test: Cumulative Firing', + actionGroups: ['default', 'other'], + async executor(alertExecutorOptions: AlertExecutorOptions) { + const { services, state } = alertExecutorOptions; + const group = 'default'; + + const runCount = (state.runCount || 0) + 1; + + times(runCount, index => { + services + .alertInstanceFactory(`instance-${index}`) + .replaceState({ instanceStateValue: true }) + .scheduleActions(group); + }); + + return { + runCount, + }; + }, + }; const neverFiringAlertType: AlertType = { id: 'test.never-firing', name: 'Test: Never firing', @@ -364,6 +387,7 @@ export default function(kibana: any) { async executor({ services, params, state }: AlertExecutorOptions) {}, }; server.plugins.alerting.setup.registerType(alwaysFiringAlertType); + server.plugins.alerting.setup.registerType(cumulativeFiringAlertType); server.plugins.alerting.setup.registerType(neverFiringAlertType); server.plugins.alerting.setup.registerType(failingAlertType); server.plugins.alerting.setup.registerType(validationAlertType); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts new file mode 100644 index 0000000000000..d95f9ea8ac0ea --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get_alert_state.ts @@ -0,0 +1,126 @@ +/* + * 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 { getUrlPrefix, ObjectRemover, getTestAlertData } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { UserAtSpaceScenarios } from '../../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function createGetAlertStateTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('getAlertState', () => { + const objectRemover = new ObjectRemover(supertest); + + afterEach(() => objectRemover.removeAll()); + + for (const scenario of UserAtSpaceScenarios) { + const { user, space } = scenario; + describe(scenario.id, () => { + it('should handle getAlertState alert request appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/state`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(200); + expect(response.body).to.key('alertInstances', 'previousStartedAt'); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it(`shouldn't getAlertState for an alert from another space`, async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(space.id, createdAlert.id, 'alert'); + + const response = await supertestWithoutAuth + .get(`${getUrlPrefix('other')}/api/alert/${createdAlert.id}/state`) + .auth(user.username, user.password); + + expect(response.statusCode).to.eql(404); + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'space_1_all at space1': + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'global_read at space1': + case 'superuser at space1': + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: `Saved object [alert/${createdAlert.id}] not found`, + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + + it(`should handle getAlertState request appropriately when alert doesn't exist`, async () => { + const response = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alert/1/state`) + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + break; + case 'global_read at space1': + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Saved object [alert/1] not found', + }); + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); + }); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 1aa084356cfa4..91b0ca0a37c92 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -15,6 +15,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./enable')); loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./get_alert_state')); loadTestFile(require.resolve('./list_alert_types')); loadTestFile(require.resolve('./mute_all')); loadTestFile(require.resolve('./mute_instance')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts new file mode 100644 index 0000000000000..053df3b7199cc --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get_alert_state.ts @@ -0,0 +1,89 @@ +/* + * 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 { Spaces } from '../../scenarios'; +import { getUrlPrefix, ObjectRemover, getTestAlertData } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createGetAlertStateTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + + describe('getAlertState', () => { + const objectRemover = new ObjectRemover(supertest); + + afterEach(() => objectRemover.removeAll()); + + it('should handle getAlertState request appropriately', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/state` + ); + + expect(response.statusCode).to.eql(200); + expect(response.body).to.key('alertInstances', 'previousStartedAt'); + }); + + it('should fetch updated state', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send({ + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: 'test.cumulative-firing', + consumer: 'bar', + schedule: { interval: '5s' }, + throttle: '5s', + actions: [], + params: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert'); + + // wait for alert to actually execute + await retry.try(async () => { + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/state` + ); + + expect(response.statusCode).to.eql(200); + expect(response.body).to.key('alertInstances', 'alertTypeState', 'previousStartedAt'); + expect(response.body.alertTypeState.runCount).to.greaterThan(1); + }); + + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/state` + ); + + expect(response.body.alertTypeState.runCount).to.greaterThan(0); + + const alertInstances = Object.entries>(response.body.alertInstances); + expect(alertInstances.length).to.eql(response.body.alertTypeState.runCount); + alertInstances.forEach(([key, value], index) => { + expect(key).to.eql(`instance-${index}`); + expect(value.state).to.eql({ instanceStateValue: true }); + }); + }); + + it(`should handle getAlertState request appropriately when alert doesn't exist`, async () => { + await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/1/state`).expect(404, { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [alert/1] not found', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index 569c0d538d473..0b7f51ac9a79b 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -15,6 +15,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./enable')); loadTestFile(require.resolve('./find')); loadTestFile(require.resolve('./get')); + loadTestFile(require.resolve('./get_alert_state')); loadTestFile(require.resolve('./list_alert_types')); loadTestFile(require.resolve('./mute_all')); loadTestFile(require.resolve('./mute_instance')); 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/uptime.ts b/x-pack/test/functional/services/uptime.ts index 1d8e0c97b99c4..938be2c71ae74 100644 --- a/x-pack/test/functional/services/uptime.ts +++ b/x-pack/test/functional/services/uptime.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function UptimeProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const browser = getService('browser'); + const retry = getService('retry'); return { async assertExists(key: string) { @@ -17,7 +18,9 @@ export function UptimeProvider({ getService }: FtrProviderContext) { } }, async monitorIdExists(key: string) { - await testSubjects.existOrFail(key); + await retry.tryForTime(10000, async () => { + await testSubjects.existOrFail(key); + }); }, async monitorPageLinkExists(monitorId: string) { await testSubjects.existOrFail(`monitor-page-link-${monitorId}`); diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js index 9e818f050c929..785fbed341423 100644 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js +++ b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js @@ -24,6 +24,14 @@ const taskManagerQuery = { }; export function initRoutes(server, taskManager, legacyTaskManager, taskTestingEvents) { + const callCluster = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser; + + async function ensureIndexIsRefreshed() { + return await callCluster('indices.refresh', { + index: '.kibana_task_manager', + }); + } + server.route({ path: '/api/sample_tasks/schedule', method: 'POST', @@ -198,19 +206,8 @@ export function initRoutes(server, taskManager, legacyTaskManager, taskTestingEv method: 'GET', async handler(request) { try { - return taskManager.fetch({ - query: { - bool: { - must: [ - { - ids: { - values: [`task:${request.params.taskId}`], - }, - }, - ], - }, - }, - }); + await ensureIndexIsRefreshed(); + return await taskManager.get(request.params.taskId); } catch (err) { return err; } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index 7ec0e9b5efa5b..e8f976d5ae6e3 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -69,7 +69,7 @@ export default function({ getService }) { .get(`/api/sample_tasks/task/${task}`) .send({ task }) .expect(200) - .then(response => response.body.docs[0]); + .then(response => response.body); } function historyDocs(taskId) { @@ -434,9 +434,7 @@ export default function({ getService }) { expect(successfulRunNowResult).to.eql({ id: originalTask.id }); await retry.try(async () => { - const [task] = (await currentTasks()).docs.filter( - taskDoc => taskDoc.id === originalTask.id - ); + const task = await currentTask(originalTask.id); expect(task.state.count).to.eql(2); }); diff --git a/x-pack/test/ui_capabilities/common/services/features.ts b/x-pack/test/ui_capabilities/common/services/features.ts index 9f644fd6d0f6e..0f796c1d0a0cc 100644 --- a/x-pack/test/ui_capabilities/common/services/features.ts +++ b/x-pack/test/ui_capabilities/common/services/features.ts @@ -22,9 +22,11 @@ export class FeaturesService { }); } - public async get(): Promise { + public async get({ ignoreValidLicenses } = { ignoreValidLicenses: false }): Promise { this.log.debug('requesting /api/features to get the features'); - const response = await this.axios.get('/api/features'); + const response = await this.axios.get( + `/api/features?ignoreValidLicenses=${ignoreValidLicenses}` + ); if (response.status !== 200) { throw new Error( diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/index.ts b/x-pack/test/ui_capabilities/spaces_only/tests/index.ts index 0b40f9716dcb4..a25838ac4f76d 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/index.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/index.ts @@ -16,7 +16,8 @@ export default function uiCapabilitesTests({ loadTestFile, getService }: FtrProv this.tags('ciGroup9'); before(async () => { - const features = await featuresService.get(); + // we're using a basic license, so if we want to disable all features, we have to ignore the valid licenses + const features = await featuresService.get({ ignoreValidLicenses: true }); for (const space of SpaceScenarios) { const disabledFeatures = space.disabledFeatures === '*' ? Object.keys(features) : space.disabledFeatures; diff --git a/yarn.lock b/yarn.lock index e4bc7fb9f851b..d242dc310e472 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20747,18 +20747,10 @@ markdown-it@^10.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" -markdown-to-jsx@^6.9.1: - version "6.9.3" - resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-6.9.3.tgz#31719e3c54517ba9805db81d53701b89f5d2ed7e" - integrity sha512-iXteiv317VZd1vk/PBH5MWMD4r0XWekoWCHRVVadBcnCtxavhtfV1UaEaQgq9KyckTv31L60ASh5ZVVrOh37Qg== - dependencies: - prop-types "^15.6.2" - unquote "^1.1.0" - -markdown-to-jsx@^6.9.3: - version "6.10.3" - resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-6.10.3.tgz#7f0946684acd321125ff2de7fd258a9b9c7c40b7" - integrity sha512-PSoUyLnW/xoW6RsxZrquSSz5eGEOTwa15H5eqp3enmrp8esmgDJmhzd6zmQ9tgAA9TxJzx1Hmf3incYU/IamoQ== +markdown-to-jsx@^6.9.1, markdown-to-jsx@^6.9.3: + version "6.11.0" + resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-6.11.0.tgz#a2e3f2bc781c3402d8bb0f8e0a12a186474623b0" + integrity sha512-RH7LCJQ4RFmPqVeZEesKaO1biRzB/k4utoofmTCp3Eiw6D7qfvK8fzZq/2bjEJAtVkfPrM5SMt5APGf2rnaKMg== dependencies: prop-types "^15.6.2" unquote "^1.1.0" @@ -24540,10 +24532,10 @@ querystring@0.2.0, querystring@^0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= -querystringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef" - integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg== +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== quick-lru@^1.0.0: version "1.1.0" @@ -30927,11 +30919,11 @@ url-parse-lax@^3.0.0: prepend-http "^2.0.0" url-parse@^1.4.3: - version "1.4.4" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.4.tgz#cac1556e95faa0303691fec5cf9d5a1bc34648f8" - integrity sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg== + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== dependencies: - querystringify "^2.0.0" + querystringify "^2.1.1" requires-port "^1.0.0" url-pattern@^1.0.3: