diff --git a/.eslintrc.js b/.eslintrc.js index 9b75c36c95abd..3161a25b70870 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -305,6 +305,8 @@ module.exports = { '!src/core/server/mocks{,.ts}', '!src/core/server/types{,.ts}', '!src/core/server/test_utils{,.ts}', + '!src/core/server/utils', // ts alias + '!src/core/server/utils/**/*', // for absolute imports until fixed in // https://github.com/elastic/kibana/issues/36096 '!src/core/server/*.test.mocks{,.ts}', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1d0f6fc50ee9b..b4563dd1f9a9c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -209,8 +209,19 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/ @elastic/kibana-alerting-services # Enterprise Search -/x-pack/plugins/enterprise_search/ @elastic/app-search-frontend @elastic/workplace-search-frontend -/x-pack/test/functional_enterprise_search/ @elastic/app-search-frontend @elastic/workplace-search-frontend +# Shared +/x-pack/plugins/enterprise_search/ @elastic/enterprise-search-frontend +/x-pack/test/functional_enterprise_search/ @elastic/enterprise-search-frontend +# App Search +/x-pack/plugins/enterprise_search/public/applications/app_search @elastic/app-search-frontend +/x-pack/plugins/enterprise_search/server/routes/app_search @elastic/app-search-frontend +/x-pack/plugins/enterprise_search/server/collectors/app_search @elastic/app-search-frontend +/x-pack/plugins/enterprise_search/server/saved_objects/app_search @elastic/app-search-frontend +# Workplace Search +/x-pack/plugins/enterprise_search/public/applications/workplace_search @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/server/routes/workplace_search @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/server/collectors/workplace_search @elastic/workplace-search-frontend +/x-pack/plugins/enterprise_search/server/saved_objects/workplace_search @elastic/workplace-search-frontend # Elasticsearch UI /src/plugins/dev_tools/ @elastic/es-ui diff --git a/.gitignore b/.gitignore index 1d12ef2a9cff3..1bbd38debbf0f 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,6 @@ apm.tsconfig.json # release notes script output report.csv report.asciidoc + +# TS incremental build cache +*.tsbuildinfo diff --git a/NOTICE.txt b/NOTICE.txt index e1552852d0349..d689abf4c4e05 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -281,6 +281,13 @@ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +--- +This product includes code in the function applyCubicBezierStyles that was +inspired by a public Codepen, which was available under a "MIT" license. + +Copyright (c) 2020 by Guillaume (https://codepen.io/guillaumethomas/pen/xxbbBKO) +MIT License http://www.opensource.org/licenses/mit-license + --- This product includes code that is adapted from mapbox-gl-js, which is available under a "BSD-3-Clause" license. diff --git a/docs/apm/apm-app-users.asciidoc b/docs/apm/apm-app-users.asciidoc index d766c866f87e4..3f0a42251304c 100644 --- a/docs/apm/apm-app-users.asciidoc +++ b/docs/apm/apm-app-users.asciidoc @@ -84,7 +84,7 @@ Here are two examples: | Allow the use of the the {beat_kib_app} | Spaces -| `Read` or `All` on Dashboards, Visualize, and Discover +| `Read` or `All` on Dashboards and Discover | Allow the user to view, edit, and create dashboards, as well as browse data. |==== diff --git a/docs/canvas/canvas-elements.asciidoc b/docs/canvas/canvas-elements.asciidoc new file mode 100644 index 0000000000000..782bae061b8c1 --- /dev/null +++ b/docs/canvas/canvas-elements.asciidoc @@ -0,0 +1,167 @@ +[role="xpack"] +[[add-canvas-elements]] +=== Add elements + +Create a story about your data by adding elements to your workpad that include images, text, charts, and more. You can create your own elements and connect them to your data sources, add saved objects, and add your own images. + +[float] +[[create-canvas-element]] +==== Create an element + +Choose the type of element you want to use, then connect it to your own data. + +. Click *Add element*, then select the element you want to use. ++ +[role="screenshot"] +image::images/canvas-element-select.gif[Canvas elements] + +. To familiarize yourself with the element, use the preconfigured data demo data. ++ +By default, most of the elements you create use demo data until you change the data source. The demo data includes a small data set that you can use to experiment with your element. + +. To connect the element to your data, select *Data*, then select one of the following data sources: + +* *{es} SQL* — Access your data in {es} using SQL syntax. For information about SQL syntax, refer to {ref}/sql-spec.html[SQL language]. + +* *{es} documents* — Access your data in {es} without using aggregations. To use, select an index and fields, and optionally enter a query using the <>. Use the *{es} documents* data source when you have low volume datasets, to view raw documents, or to plot exact, non-aggregated values on a chart. + +* *Timelion* — Access your time series data using <> queries. To use Timelion queries, you can enter a query using the <>. + +Each element can display a different data source. Pages and workpads often contain multiple data sources. + +[float] +[[canvas-add-object]] +==== Add a saved object + +Add <> to your workpad, such as maps and visualizations. + +. Click *Add element > Add from Visualize Library*. + +. Select the saved object you want to add. ++ +[role="screenshot"] +image::images/canvas-map-embed.gif[] + +. To use the customization options, click the panel menu, then select one of the following options: + +* *Edit map* — Opens <> or <> so that you can edit the original saved object. + +* *Edit panel title* — Adds a title to the saved object. + +* *Customize time range* — Exposes a time filter dedicated to the saved object. + +* *Inspect* — Allows you to drill down into the element data. + +[float] +[[canvas-add-image]] +==== Add your own image + +To personalize your workpad, add your own logos and graphics. + +. Click *Add element > Manage assets*. + +. On the *Manage workpad assets* window, drag and drop your images. + +. To add the image to the workpad, click the *Create image element* icon. ++ +[role="screenshot"] +image::images/canvas-add-image.gif[] + +[float] +[[move-canvas-elements]] +==== Organize elements + +Move and resize your elements to meet your design needs. + +* To move, click and hold the element, then drag to the new location. + +* To move by 1 pixel, select the element, press and hold Shift, then use your arrow keys. + +* To move by 10 pixels, select the element, then use your arrow keys. + +* To resize, click and drag the resize handles to the new dimensions. + +[float] +[[format-canvas-elements]] +==== Format elements + +For consistency and readability across your workpad pages, align, distribute, and reorder elements. + +To align two or more elements: + +. Press and hold Shift, then select the elements you want to align. + +. Click *Edit > Alignment*, then select the alignment option. + +To distribute three or more elements: + +. Press and hold Shift, then select the elements you want to distribute. + +. Click *Edit > Distribution*, then select the distribution option. + +To reorder elements: + +. Select the element you want to reorder. + +. Click *Edit > Order*, then select the order option. + +[float] +[[data-display]] +==== Change the element display options + +Each element has its own display options to fit your design needs. + +To choose the display options, click *Display*, then make your changes. + +To define the appearance of the container and border: + +. Next to *Element style*, click *+*, then select *Container style*. + +. Expand *Container style*. + +. Change the *Appearance* and *Border* options. + +To apply CSS overrides: + +. Next to *Element style*, click *+*, then select *CSS*. + +. Enter the *CSS*. ++ +For example, to center the Markdown element, enter: ++ +[source,text] +-------------------------------------------------- +.canvasRenderEl h1 { +text.align: center; +} +-------------------------------------------------- + +. Click *Apply stylesheet*. + +[float] +[[save-elements]] +==== Save elements + +To use the elements across all workpads, save the elements. + +When you're ready to save your element, select the element, then click *Edit > Save as new element*. + +[role="screenshot"] +image::images/canvas_save_element.png[] + +To save a group of elements, press and hold Shift, select the elements you want to save, then click *Edit > Save as new element*. + +To access your saved elements, click *Add element > My elements*. + +[float] +[[delete-elements]] +==== Delete elements + +When you no longer need an element, delete it from your workpad. + +. Select the element you want to delete. + +. Click *Edit > Delete*. ++ +[role="screenshot"] +image::images/canvas_element_options.png[] diff --git a/docs/developer/contributing/development-accessibility-tests.asciidoc b/docs/developer/contributing/development-accessibility-tests.asciidoc index facf7ff14a6c1..584d779bc7de6 100644 --- a/docs/developer/contributing/development-accessibility-tests.asciidoc +++ b/docs/developer/contributing/development-accessibility-tests.asciidoc @@ -1,23 +1,104 @@ [[development-accessibility-tests]] == Automated Accessibility Testing + +To write an accessibility test, use the provided accessibility service `getService('a11y')`. Accessibility tests are fairly straightforward to write as https://github.com/dequelabs/axe-core[axe] does most of the heavy lifting. Navigate to the UI that you need to test, then call `testAppSnapshot();` from the service imported earlier to make sure axe finds no failures. Navigate through every portion of the UI for the best coverage. + +An example test might look like this: +[source,js] +---- +export default function ({ getService, getPageObjects }) { + const { common, home } = getPageObjects(['common', 'home']); + const a11y = getService('a11y'); /* this is the wrapping service around axe */ + + describe('Kibana Home', () => { + before(async () => { + await common.navigateToApp('home'); /* navigates to the page we want to test */ + }); + + it('Kibana Home view', async () => { + await retry.waitFor( + 'home page visible', + async () => await testSubjects.exists('homeApp') + ); /* confirm you're on the correct page and that it's loaded */ + await a11y.testAppSnapshot(); /* this expects that there are no failures found by axe */ + }); + + /** + * If these tests were added by our QA team, tests that fail that require significant app code + * changes to be fixed will be skipped with a corresponding issue label with more info + */ + // Skipped due to https://github.com/elastic/kibana/issues/99999 + it.skip('all plugins view page meets a11y requirements', async () => { + await home.clickAllKibanaPlugins(); + await a11y.testAppSnapshot(); + }); + + /** + * Testing all the versions and different views of of a page is important to get good + * coverage. Things like empty states, different license levels, different permissions, and + * loaded data can all significantly change the UI which necessitates their own test. + */ + it('Add Kibana sample data page', async () => { + await common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await a11y.testAppSnapshot(); + }); + }); +} +---- + +=== Running tests To run the tests locally: [arabic] -. In one terminal window run -`node scripts/functional_tests_server --config test/accessibility/config.ts` -. In another terminal window run -`node scripts/functional_test_runner.js --config test/accessibility/config.ts` +. In one terminal window run: ++ +[source,shell] +----------- +node scripts/functional_tests_server --config test/accessibility/config.ts +----------- + +. When the server prints that it is ready, in another terminal window run: ++ +[source,shell] +----------- +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 {kib} at -`localhost:5620`. - The testing is done using https://github.com/dequelabs/axe-core[axe]. -The same thing that runs in CI, can be run locally using their browser -plugins: +You can run the same thing that runs CI using browser plugins: * https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US[Chrome] -* https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/[Firefox] \ No newline at end of file +* https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/[Firefox] + +=== Anatomy of a failure + +Failures can seem confusing if you've never seen one before. Here is a breakdown of what a failure coming from CI might look like: +[source,bash] +---- +1) Dashboard + create dashboard button: + + Error: a11y report: + + VIOLATION + [aria-hidden-focus]: Ensures aria-hidden elements do not contain focusable elements + Help: https://dequeuniversity.com/rules/axe/3.5/aria-hidden-focus?application=axeAPI + Elements: + - #example + at Accessibility.testAxeReport (test/accessibility/services/a11y/a11y.ts:90:15) + at Accessibility.testAppSnapshot (test/accessibility/services/a11y/a11y.ts:58:18) + at process._tickCallback (internal/process/next_tick.js:68:7) +---- + + +* "Dashboard" and "create dashboard button" are the names of the test suite and specific test that failed. +* Always in brackets, "[aria-hidden-focus]" is the name of the axe rule that failed, followed by a short description. +* "Help: " links to the axe documentation for that rule, including severity, remediation tips, and good and bad code examples. +* "Elements:" points to where in the DOM the failure originated (using CSS selector syntax). In this example, the problem came from an element with the ID `example`. If the selector is too complicated to find the source of the problem, use the browser plugins mentioned earlier to locate it. If you have a general idea where the issue is coming from, you can also try adding unique IDs to the page to narrow down the location. +* The stack trace points to the internals of axe. The stack trace is there in case the test failure is a bug in axe and not in your code, although this is unlikely. diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.capabilities.md b/docs/development/core/public/kibana-plugin-core-public.app.capabilities.md similarity index 59% rename from docs/development/core/public/kibana-plugin-core-public.appbase.capabilities.md rename to docs/development/core/public/kibana-plugin-core-public.app.capabilities.md index 3dd440c4253b3..4a027a6ab132c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.capabilities.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.capabilities.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [capabilities](./kibana-plugin-core-public.appbase.capabilities.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [capabilities](./kibana-plugin-core-public.app.capabilities.md) -## AppBase.capabilities property +## App.capabilities property Custom capabilities defined by the app. diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.category.md b/docs/development/core/public/kibana-plugin-core-public.app.category.md similarity index 67% rename from docs/development/core/public/kibana-plugin-core-public.appbase.category.md rename to docs/development/core/public/kibana-plugin-core-public.app.category.md index 29532a15747e1..a1e74f2bcf5e2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.category.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.category.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [category](./kibana-plugin-core-public.appbase.category.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [category](./kibana-plugin-core-public.app.category.md) -## AppBase.category property +## App.category property The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md b/docs/development/core/public/kibana-plugin-core-public.app.defaultpath.md similarity index 77% rename from docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md rename to docs/development/core/public/kibana-plugin-core-public.app.defaultpath.md index 51492756ef232..3c952ec053e62 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.defaultpath.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [defaultPath](./kibana-plugin-core-public.app.defaultpath.md) -## AppBase.defaultPath property +## App.defaultPath property Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the `path` option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.euiicontype.md b/docs/development/core/public/kibana-plugin-core-public.app.euiicontype.md similarity index 63% rename from docs/development/core/public/kibana-plugin-core-public.appbase.euiicontype.md rename to docs/development/core/public/kibana-plugin-core-public.app.euiicontype.md index e5bfa38097361..ff79d832f92e2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.euiicontype.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.euiicontype.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [euiIconType](./kibana-plugin-core-public.appbase.euiicontype.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [euiIconType](./kibana-plugin-core-public.app.euiicontype.md) -## AppBase.euiIconType property +## App.euiIconType property A EUI iconType that will be used for the app's icon. This icon takes precendence over the `icon` property. diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.icon.md b/docs/development/core/public/kibana-plugin-core-public.app.icon.md similarity index 65% rename from docs/development/core/public/kibana-plugin-core-public.appbase.icon.md rename to docs/development/core/public/kibana-plugin-core-public.app.icon.md index 0bd67922dc39c..98260da5d2021 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.icon.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.icon.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [icon](./kibana-plugin-core-public.appbase.icon.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [icon](./kibana-plugin-core-public.app.icon.md) -## AppBase.icon property +## App.icon property A URL to an image file used as an icon. Used as a fallback if `euiIconType` is not provided. diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.id.md b/docs/development/core/public/kibana-plugin-core-public.app.id.md similarity index 61% rename from docs/development/core/public/kibana-plugin-core-public.appbase.id.md rename to docs/development/core/public/kibana-plugin-core-public.app.id.md index 6c0ec462fa16b..9899cfc0cf572 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.id.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.id.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [id](./kibana-plugin-core-public.appbase.id.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [id](./kibana-plugin-core-public.app.id.md) -## AppBase.id property +## App.id property The unique identifier of the application diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index 8dd60972549f9..7bdee9dc4c53e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -4,12 +4,11 @@ ## App interface -Extension of [common app properties](./kibana-plugin-core-public.appbase.md) with the mount function. Signature: ```typescript -export interface App extends AppBase +export interface App ``` ## Properties @@ -17,7 +16,19 @@ export interface App extends AppBase | Property | Type | Description | | --- | --- | --- | | [appRoute](./kibana-plugin-core-public.app.approute.md) | string | Override the application's routing path from /app/${id}. Must be unique across registered applications. Should not include the base path from HTTP. | +| [capabilities](./kibana-plugin-core-public.app.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | +| [category](./kibana-plugin-core-public.app.category.md) | AppCategory | The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference | | [chromeless](./kibana-plugin-core-public.app.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | +| [defaultPath](./kibana-plugin-core-public.app.defaultpath.md) | string | Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the path option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. | +| [euiIconType](./kibana-plugin-core-public.app.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | | [exactRoute](./kibana-plugin-core-public.app.exactroute.md) | boolean | If set to true, the application's route will only be checked against an exact match. Defaults to false. | +| [icon](./kibana-plugin-core-public.app.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | +| [id](./kibana-plugin-core-public.app.id.md) | string | The unique identifier of the application | | [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState> | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-core-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md). | +| [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | +| [order](./kibana-plugin-core-public.app.order.md) | number | An ordinal used to sort nav links relative to one another for display. | +| [status](./kibana-plugin-core-public.app.status.md) | AppStatus | The initial status of the application. Defaulting to accessible | +| [title](./kibana-plugin-core-public.app.title.md) | string | The title of the application. | +| [tooltip](./kibana-plugin-core-public.app.tooltip.md) | string | A tooltip shown when hovering over app link. | +| [updater$](./kibana-plugin-core-public.app.updater_.md) | Observable<AppUpdater> | An [AppUpdater](./kibana-plugin-core-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) at runtime. | diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.navlinkstatus.md b/docs/development/core/public/kibana-plugin-core-public.app.navlinkstatus.md similarity index 70% rename from docs/development/core/public/kibana-plugin-core-public.appbase.navlinkstatus.md rename to docs/development/core/public/kibana-plugin-core-public.app.navlinkstatus.md index decfb235b2858..c01a26e42e237 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.navlinkstatus.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.navlinkstatus.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [navLinkStatus](./kibana-plugin-core-public.appbase.navlinkstatus.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) -## AppBase.navLinkStatus property +## App.navLinkStatus property The initial status of the application's navLink. Defaulting to `visible` if `status` is `accessible` and `hidden` if status is `inaccessible` See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.order.md b/docs/development/core/public/kibana-plugin-core-public.app.order.md similarity index 63% rename from docs/development/core/public/kibana-plugin-core-public.appbase.order.md rename to docs/development/core/public/kibana-plugin-core-public.app.order.md index 606a40e72d592..bb6be116b6b58 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.order.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.order.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [order](./kibana-plugin-core-public.appbase.order.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [order](./kibana-plugin-core-public.app.order.md) -## AppBase.order property +## App.order property An ordinal used to sort nav links relative to one another for display. diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.status.md b/docs/development/core/public/kibana-plugin-core-public.app.status.md similarity index 62% rename from docs/development/core/public/kibana-plugin-core-public.appbase.status.md rename to docs/development/core/public/kibana-plugin-core-public.app.status.md index 4d6ba6ebd955e..caa6ff1dcac9e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.status.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.status.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [status](./kibana-plugin-core-public.appbase.status.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [status](./kibana-plugin-core-public.app.status.md) -## AppBase.status property +## App.status property The initial status of the application. Defaulting to `accessible` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.title.md b/docs/development/core/public/kibana-plugin-core-public.app.title.md similarity index 59% rename from docs/development/core/public/kibana-plugin-core-public.appbase.title.md rename to docs/development/core/public/kibana-plugin-core-public.app.title.md index d6058badee8e8..c705e3ab8d2b1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.title.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.title.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [title](./kibana-plugin-core-public.appbase.title.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [title](./kibana-plugin-core-public.app.title.md) -## AppBase.title property +## App.title property The title of the application. diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.tooltip.md b/docs/development/core/public/kibana-plugin-core-public.app.tooltip.md similarity index 60% rename from docs/development/core/public/kibana-plugin-core-public.appbase.tooltip.md rename to docs/development/core/public/kibana-plugin-core-public.app.tooltip.md index 0c0b0840eb921..e901de0fdccc9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.tooltip.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.tooltip.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [tooltip](./kibana-plugin-core-public.appbase.tooltip.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [tooltip](./kibana-plugin-core-public.app.tooltip.md) -## AppBase.tooltip property +## App.tooltip property A tooltip shown when hovering over app link. diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.updater_.md b/docs/development/core/public/kibana-plugin-core-public.app.updater_.md similarity index 86% rename from docs/development/core/public/kibana-plugin-core-public.appbase.updater_.md rename to docs/development/core/public/kibana-plugin-core-public.app.updater_.md index c2c572755f9b2..67acccbd02965 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.updater_.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.updater_.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [updater$](./kibana-plugin-core-public.appbase.updater_.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [updater$](./kibana-plugin-core-public.app.updater_.md) -## AppBase.updater$ property +## App.updater$ property An [AppUpdater](./kibana-plugin-core-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) at runtime. diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.chromeless.md b/docs/development/core/public/kibana-plugin-core-public.appbase.chromeless.md deleted file mode 100644 index 793eab4b5bdfa..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.chromeless.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [chromeless](./kibana-plugin-core-public.appbase.chromeless.md) - -## AppBase.chromeless property - -Hide the UI chrome when the application is mounted. Defaults to `false`. Takes precedence over chrome service visibility settings. - -Signature: - -```typescript -chromeless?: boolean; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.md b/docs/development/core/public/kibana-plugin-core-public.appbase.md deleted file mode 100644 index 7b624f12ac1df..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.md +++ /dev/null @@ -1,31 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) - -## AppBase interface - - -Signature: - -```typescript -export interface AppBase -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [capabilities](./kibana-plugin-core-public.appbase.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | -| [category](./kibana-plugin-core-public.appbase.category.md) | AppCategory | The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference | -| [chromeless](./kibana-plugin-core-public.appbase.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | -| [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md) | string | Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the path option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. | -| [euiIconType](./kibana-plugin-core-public.appbase.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | -| [icon](./kibana-plugin-core-public.appbase.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | -| [id](./kibana-plugin-core-public.appbase.id.md) | string | The unique identifier of the application | -| [navLinkStatus](./kibana-plugin-core-public.appbase.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | -| [order](./kibana-plugin-core-public.appbase.order.md) | number | An ordinal used to sort nav links relative to one another for display. | -| [status](./kibana-plugin-core-public.appbase.status.md) | AppStatus | The initial status of the application. Defaulting to accessible | -| [title](./kibana-plugin-core-public.appbase.title.md) | string | The title of the application. | -| [tooltip](./kibana-plugin-core-public.appbase.tooltip.md) | string | A tooltip shown when hovering over app link. | -| [updater$](./kibana-plugin-core-public.appbase.updater_.md) | Observable<AppUpdater> | An [AppUpdater](./kibana-plugin-core-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) at runtime. | - diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.applications_.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.applications_.md index d428faa500faf..bcc5435f35951 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.applications_.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.applications_.md @@ -9,7 +9,7 @@ Observable emitting the list of currently registered apps and their associated s Signature: ```typescript -applications$: Observable>; +applications$: Observable>; ``` ## Remarks diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.md index 896de2de32dd5..00318f32984e9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.md @@ -15,7 +15,7 @@ export interface ApplicationStart | Property | Type | Description | | --- | --- | --- | -| [applications$](./kibana-plugin-core-public.applicationstart.applications_.md) | Observable<ReadonlyMap<string, PublicAppInfo | PublicLegacyAppInfo>> | Observable emitting the list of currently registered apps and their associated status. | +| [applications$](./kibana-plugin-core-public.applicationstart.applications_.md) | Observable<ReadonlyMap<string, PublicAppInfo>> | Observable emitting the list of currently registered apps and their associated status. | | [capabilities](./kibana-plugin-core-public.applicationstart.capabilities.md) | RecursiveReadonly<Capabilities> | Gets the read-only capabilities. | | [currentAppId$](./kibana-plugin-core-public.applicationstart.currentappid_.md) | Observable<string | undefined> | An observable that emits the current application id and each subsequent id update. | diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md index 3d8b5d115c8a2..1232b7f940255 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md @@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug Signature: ```typescript -export declare type AppUpdatableFields = Pick; +export declare type AppUpdatableFields = Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdater.md b/docs/development/core/public/kibana-plugin-core-public.appupdater.md index a1c1424132da6..744c52f221da7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appupdater.md +++ b/docs/development/core/public/kibana-plugin-core-public.appupdater.md @@ -9,5 +9,5 @@ Updater for applications. see [ApplicationSetup](./kibana-plugin-core-public.app Signature: ```typescript -export declare type AppUpdater = (app: AppBase) => Partial | undefined; +export declare type AppUpdater = (app: App) => Partial | undefined; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.active.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.active.md deleted file mode 100644 index fb8a6eb691b42..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.active.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeNavLink](./kibana-plugin-core-public.chromenavlink.md) > [active](./kibana-plugin-core-public.chromenavlink.active.md) - -## ChromeNavLink.active property - -> Warning: This API is now obsolete. -> -> - -Indicates whether or not this app is currently on the screen. - -Signature: - -```typescript -readonly active?: boolean; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.disabled.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.disabled.md index 9e1aefb79ad39..2b4d22be187f9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.disabled.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.disabled.md @@ -4,10 +4,6 @@ ## ChromeNavLink.disabled property -> Warning: This API is now obsolete. -> -> - Disables a link from being clickable. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.disablesuburltracking.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.disablesuburltracking.md deleted file mode 100644 index 843fd959d262a..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.disablesuburltracking.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeNavLink](./kibana-plugin-core-public.chromenavlink.md) > [disableSubUrlTracking](./kibana-plugin-core-public.chromenavlink.disablesuburltracking.md) - -## ChromeNavLink.disableSubUrlTracking property - -> Warning: This API is now obsolete. -> -> - -A flag that tells legacy chrome to ignore the link when tracking sub-urls - -Signature: - -```typescript -readonly disableSubUrlTracking?: boolean; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md index a8af0c997ca78..f51fa7e5b1355 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.href.md @@ -9,5 +9,5 @@ Settled state between `url`, `baseUrl`, and `active` Signature: ```typescript -readonly href?: string; +readonly href: string; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.linktolastsuburl.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.linktolastsuburl.md deleted file mode 100644 index 0b6d6ae129744..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.linktolastsuburl.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeNavLink](./kibana-plugin-core-public.chromenavlink.md) > [linkToLastSubUrl](./kibana-plugin-core-public.chromenavlink.linktolastsuburl.md) - -## ChromeNavLink.linkToLastSubUrl property - -> Warning: This API is now obsolete. -> -> - -Whether or not the subUrl feature should be enabled. - -Signature: - -```typescript -readonly linkToLastSubUrl?: boolean; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md index 0349e865bff97..dfe8f119505aa 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md @@ -15,20 +15,16 @@ export interface ChromeNavLink | Property | Type | Description | | --- | --- | --- | -| [active](./kibana-plugin-core-public.chromenavlink.active.md) | boolean | Indicates whether or not this app is currently on the screen. | | [baseUrl](./kibana-plugin-core-public.chromenavlink.baseurl.md) | string | The base route used to open the root of an application. | | [category](./kibana-plugin-core-public.chromenavlink.category.md) | AppCategory | The category the app lives in | | [disabled](./kibana-plugin-core-public.chromenavlink.disabled.md) | boolean | Disables a link from being clickable. | -| [disableSubUrlTracking](./kibana-plugin-core-public.chromenavlink.disablesuburltracking.md) | boolean | A flag that tells legacy chrome to ignore the link when tracking sub-urls | | [euiIconType](./kibana-plugin-core-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precedence over the icon property. | | [hidden](./kibana-plugin-core-public.chromenavlink.hidden.md) | boolean | Hides a link from the navigation. | | [href](./kibana-plugin-core-public.chromenavlink.href.md) | string | Settled state between url, baseUrl, and active | | [icon](./kibana-plugin-core-public.chromenavlink.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | | [id](./kibana-plugin-core-public.chromenavlink.id.md) | string | A unique identifier for looking up links. | -| [linkToLastSubUrl](./kibana-plugin-core-public.chromenavlink.linktolastsuburl.md) | boolean | Whether or not the subUrl feature should be enabled. | | [order](./kibana-plugin-core-public.chromenavlink.order.md) | number | An ordinal used to sort nav links relative to one another for display. | -| [subUrlBase](./kibana-plugin-core-public.chromenavlink.suburlbase.md) | string | A url base that legacy apps can set to match deep URLs to an application. | | [title](./kibana-plugin-core-public.chromenavlink.title.md) | string | The title of the application. | | [tooltip](./kibana-plugin-core-public.chromenavlink.tooltip.md) | string | A tooltip shown when hovering over an app link. | -| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string | The route used to open the [default path](./kibana-plugin-core-public.appbase.defaultpath.md) of an application. If unset, baseUrl will be used instead. | +| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string | The route used to open the of an application. If unset, baseUrl will be used instead. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.suburlbase.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.suburlbase.md deleted file mode 100644 index 047a1d83b137f..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.suburlbase.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeNavLink](./kibana-plugin-core-public.chromenavlink.md) > [subUrlBase](./kibana-plugin-core-public.chromenavlink.suburlbase.md) - -## ChromeNavLink.subUrlBase property - -> Warning: This API is now obsolete. -> -> - -A url base that legacy apps can set to match deep URLs to an application. - -Signature: - -```typescript -readonly subUrlBase?: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md index 1e0b890015993..833930c494786 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md @@ -4,7 +4,7 @@ ## ChromeNavLink.url property -The route used to open the [default path](./kibana-plugin-core-public.appbase.defaultpath.md) of an application. If unset, `baseUrl` will be used instead. +The route used to open the of an application. If unset, `baseUrl` will be used instead. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.update.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.update.md index 5741a4c98f895..7948f2f8543fd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.update.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlinks.update.md @@ -6,7 +6,7 @@ > Warning: This API is now obsolete. > -> Uses the [AppBase.updater$](./kibana-plugin-core-public.appbase.updater_.md) property when registering your application with [ApplicationSetup.register()](./kibana-plugin-core-public.applicationsetup.register.md) instead. +> Uses the property when registering your application with [ApplicationSetup.register()](./kibana-plugin-core-public.applicationsetup.register.md) instead. > Update the navlink for the given id with the updated attributes. Returns the updated navlink or `undefined` if it does not exist. diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md index bd5a1399cded7..0445bb28bb355 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlinkupdateablefields.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type ChromeNavLinkUpdateableFields = Partial>; +export declare type ChromeNavLinkUpdateableFields = Partial>; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.coresetup.injectedmetadata.md b/docs/development/core/public/kibana-plugin-core-public.coresetup.injectedmetadata.md index b8f2699b677b0..8c845c621e0d7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.coresetup.injectedmetadata.md +++ b/docs/development/core/public/kibana-plugin-core-public.coresetup.injectedmetadata.md @@ -8,7 +8,7 @@ > > -exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. Use the legacy platform API instead. +exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.coresetup.md b/docs/development/core/public/kibana-plugin-core-public.coresetup.md index 870fa33dce900..b9f97b83af88f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-core-public.coresetup.md @@ -21,7 +21,7 @@ export interface CoreSetupFatalErrorsSetup | [FatalErrorsSetup](./kibana-plugin-core-public.fatalerrorssetup.md) | | [getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md) | StartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-public.startservicesaccessor.md) | | [http](./kibana-plugin-core-public.coresetup.http.md) | HttpSetup | [HttpSetup](./kibana-plugin-core-public.httpsetup.md) | -| [injectedMetadata](./kibana-plugin-core-public.coresetup.injectedmetadata.md) | {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
} | exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. Use the legacy platform API instead. | +| [injectedMetadata](./kibana-plugin-core-public.coresetup.injectedmetadata.md) | {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
} | exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. | | [notifications](./kibana-plugin-core-public.coresetup.notifications.md) | NotificationsSetup | [NotificationsSetup](./kibana-plugin-core-public.notificationssetup.md) | | [uiSettings](./kibana-plugin-core-public.coresetup.uisettings.md) | IUiSettingsClient | [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.injectedmetadata.md b/docs/development/core/public/kibana-plugin-core-public.corestart.injectedmetadata.md index 45f9349ae8c61..4e9bf7c4bc0d5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.corestart.injectedmetadata.md +++ b/docs/development/core/public/kibana-plugin-core-public.corestart.injectedmetadata.md @@ -8,7 +8,7 @@ > > -exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. Use the legacy platform API instead. +exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.md b/docs/development/core/public/kibana-plugin-core-public.corestart.md index cb4a825a825b1..a7b45b318d2c9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.corestart.md @@ -22,7 +22,7 @@ export interface CoreStart | [fatalErrors](./kibana-plugin-core-public.corestart.fatalerrors.md) | FatalErrorsStart | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | | [http](./kibana-plugin-core-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-core-public.httpstart.md) | | [i18n](./kibana-plugin-core-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-core-public.i18nstart.md) | -| [injectedMetadata](./kibana-plugin-core-public.corestart.injectedmetadata.md) | {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
} | exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. Use the legacy platform API instead. | +| [injectedMetadata](./kibana-plugin-core-public.corestart.injectedmetadata.md) | {
getInjectedVar: (name: string, defaultValue?: any) => unknown;
} | exposed temporarily until https://github.com/elastic/kibana/issues/41990 done use \*only\* to retrieve config values. There is no way to set injected values in the new platform. | | [notifications](./kibana-plugin-core-public.corestart.notifications.md) | NotificationsStart | [NotificationsStart](./kibana-plugin-core-public.notificationsstart.md) | | [overlays](./kibana-plugin-core-public.corestart.overlays.md) | OverlayStart | [OverlayStart](./kibana-plugin-core-public.overlaystart.md) | | [savedObjects](./kibana-plugin-core-public.corestart.savedobjects.md) | SavedObjectsStart | [SavedObjectsStart](./kibana-plugin-core-public.savedobjectsstart.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.appurl.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.appurl.md deleted file mode 100644 index 292bf29962839..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacyapp.appurl.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) > [appUrl](./kibana-plugin-core-public.legacyapp.appurl.md) - -## LegacyApp.appUrl property - -Signature: - -```typescript -appUrl: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.disablesuburltracking.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.disablesuburltracking.md deleted file mode 100644 index af4d0eb7969d3..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacyapp.disablesuburltracking.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) > [disableSubUrlTracking](./kibana-plugin-core-public.legacyapp.disablesuburltracking.md) - -## LegacyApp.disableSubUrlTracking property - -Signature: - -```typescript -disableSubUrlTracking?: boolean; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.linktolastsuburl.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.linktolastsuburl.md deleted file mode 100644 index fa1314b74fd83..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacyapp.linktolastsuburl.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) > [linkToLastSubUrl](./kibana-plugin-core-public.legacyapp.linktolastsuburl.md) - -## LegacyApp.linkToLastSubUrl property - -Signature: - -```typescript -linkToLastSubUrl?: boolean; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.md deleted file mode 100644 index 06533aaa99170..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacyapp.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) - -## LegacyApp interface - - -Signature: - -```typescript -export interface LegacyApp extends AppBase -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [appUrl](./kibana-plugin-core-public.legacyapp.appurl.md) | string | | -| [disableSubUrlTracking](./kibana-plugin-core-public.legacyapp.disablesuburltracking.md) | boolean | | -| [linkToLastSubUrl](./kibana-plugin-core-public.legacyapp.linktolastsuburl.md) | boolean | | -| [subUrlBase](./kibana-plugin-core-public.legacyapp.suburlbase.md) | string | | - diff --git a/docs/development/core/public/kibana-plugin-core-public.legacyapp.suburlbase.md b/docs/development/core/public/kibana-plugin-core-public.legacyapp.suburlbase.md deleted file mode 100644 index 44a1e52ccd244..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacyapp.suburlbase.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyApp](./kibana-plugin-core-public.legacyapp.md) > [subUrlBase](./kibana-plugin-core-public.legacyapp.suburlbase.md) - -## LegacyApp.subUrlBase property - -Signature: - -```typescript -subUrlBase?: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacycoresetup.injectedmetadata.md b/docs/development/core/public/kibana-plugin-core-public.legacycoresetup.injectedmetadata.md deleted file mode 100644 index 4014d27907e98..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacycoresetup.injectedmetadata.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyCoreSetup](./kibana-plugin-core-public.legacycoresetup.md) > [injectedMetadata](./kibana-plugin-core-public.legacycoresetup.injectedmetadata.md) - -## LegacyCoreSetup.injectedMetadata property - -> Warning: This API is now obsolete. -> -> - -Signature: - -```typescript -injectedMetadata: InjectedMetadataSetup; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacycoresetup.md b/docs/development/core/public/kibana-plugin-core-public.legacycoresetup.md deleted file mode 100644 index 26220accbfaf3..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacycoresetup.md +++ /dev/null @@ -1,28 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyCoreSetup](./kibana-plugin-core-public.legacycoresetup.md) - -## LegacyCoreSetup interface - -> Warning: This API is now obsolete. -> -> - -Setup interface exposed to the legacy platform via the `ui/new_platform` module. - -Signature: - -```typescript -export interface LegacyCoreSetup extends CoreSetup -``` - -## Remarks - -Some methods are not supported in the legacy platform and while present to make this type compatibile with [CoreSetup](./kibana-plugin-core-public.coresetup.md), unsupported methods will throw exceptions when called. - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [injectedMetadata](./kibana-plugin-core-public.legacycoresetup.injectedmetadata.md) | InjectedMetadataSetup | | - diff --git a/docs/development/core/public/kibana-plugin-core-public.legacycorestart.injectedmetadata.md b/docs/development/core/public/kibana-plugin-core-public.legacycorestart.injectedmetadata.md deleted file mode 100644 index 288b288b1814d..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacycorestart.injectedmetadata.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyCoreStart](./kibana-plugin-core-public.legacycorestart.md) > [injectedMetadata](./kibana-plugin-core-public.legacycorestart.injectedmetadata.md) - -## LegacyCoreStart.injectedMetadata property - -> Warning: This API is now obsolete. -> -> - -Signature: - -```typescript -injectedMetadata: InjectedMetadataStart; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacycorestart.md b/docs/development/core/public/kibana-plugin-core-public.legacycorestart.md deleted file mode 100644 index 7714d0f325d2c..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacycorestart.md +++ /dev/null @@ -1,28 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyCoreStart](./kibana-plugin-core-public.legacycorestart.md) - -## LegacyCoreStart interface - -> Warning: This API is now obsolete. -> -> - -Start interface exposed to the legacy platform via the `ui/new_platform` module. - -Signature: - -```typescript -export interface LegacyCoreStart extends CoreStart -``` - -## Remarks - -Some methods are not supported in the legacy platform and while present to make this type compatibile with [CoreStart](./kibana-plugin-core-public.corestart.md), unsupported methods will throw exceptions when called. - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [injectedMetadata](./kibana-plugin-core-public.legacycorestart.injectedmetadata.md) | InjectedMetadataStart | | - diff --git a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.category.md b/docs/development/core/public/kibana-plugin-core-public.legacynavlink.category.md deleted file mode 100644 index a70aac70067de..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.category.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) > [category](./kibana-plugin-core-public.legacynavlink.category.md) - -## LegacyNavLink.category property - -Signature: - -```typescript -category?: AppCategory; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.euiicontype.md b/docs/development/core/public/kibana-plugin-core-public.legacynavlink.euiicontype.md deleted file mode 100644 index b360578f98cf1..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.euiicontype.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) > [euiIconType](./kibana-plugin-core-public.legacynavlink.euiicontype.md) - -## LegacyNavLink.euiIconType property - -Signature: - -```typescript -euiIconType?: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.icon.md b/docs/development/core/public/kibana-plugin-core-public.legacynavlink.icon.md deleted file mode 100644 index c2c6f89be0d78..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.icon.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) > [icon](./kibana-plugin-core-public.legacynavlink.icon.md) - -## LegacyNavLink.icon property - -Signature: - -```typescript -icon?: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.id.md b/docs/development/core/public/kibana-plugin-core-public.legacynavlink.id.md deleted file mode 100644 index fc79b6b4bd6dd..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) > [id](./kibana-plugin-core-public.legacynavlink.id.md) - -## LegacyNavLink.id property - -Signature: - -```typescript -id: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.md b/docs/development/core/public/kibana-plugin-core-public.legacynavlink.md deleted file mode 100644 index b6402f991f965..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) - -## LegacyNavLink interface - - -Signature: - -```typescript -export interface LegacyNavLink -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [category](./kibana-plugin-core-public.legacynavlink.category.md) | AppCategory | | -| [euiIconType](./kibana-plugin-core-public.legacynavlink.euiicontype.md) | string | | -| [icon](./kibana-plugin-core-public.legacynavlink.icon.md) | string | | -| [id](./kibana-plugin-core-public.legacynavlink.id.md) | string | | -| [order](./kibana-plugin-core-public.legacynavlink.order.md) | number | | -| [title](./kibana-plugin-core-public.legacynavlink.title.md) | string | | -| [url](./kibana-plugin-core-public.legacynavlink.url.md) | string | | - diff --git a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.order.md b/docs/development/core/public/kibana-plugin-core-public.legacynavlink.order.md deleted file mode 100644 index 6ad3081b81d4b..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.order.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) > [order](./kibana-plugin-core-public.legacynavlink.order.md) - -## LegacyNavLink.order property - -Signature: - -```typescript -order: number; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.title.md b/docs/development/core/public/kibana-plugin-core-public.legacynavlink.title.md deleted file mode 100644 index 70b0e37729f26..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.title.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) > [title](./kibana-plugin-core-public.legacynavlink.title.md) - -## LegacyNavLink.title property - -Signature: - -```typescript -title: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.url.md b/docs/development/core/public/kibana-plugin-core-public.legacynavlink.url.md deleted file mode 100644 index 7e543f4a90c1d..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.legacynavlink.url.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) > [url](./kibana-plugin-core-public.legacynavlink.url.md) - -## LegacyNavLink.url property - -Signature: - -```typescript -url: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index c931ce544f5d5..08b12190ef638 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -41,8 +41,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | Interface | Description | | --- | --- | -| [App](./kibana-plugin-core-public.app.md) | Extension of [common app properties](./kibana-plugin-core-public.appbase.md) with the mount function. | -| [AppBase](./kibana-plugin-core-public.appbase.md) | | +| [App](./kibana-plugin-core-public.app.md) | | | [AppCategory](./kibana-plugin-core-public.appcategory.md) | A category definition for nav links to know where to sort them in the left hand nav | | [AppLeaveConfirmAction](./kibana-plugin-core-public.appleaveconfirmaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md) to show a confirmation message when trying to leave an application.See | | [AppLeaveDefaultAction](./kibana-plugin-core-public.appleavedefaultaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md) to execute the default behaviour when leaving the application.See | @@ -90,10 +89,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IHttpResponseInterceptorOverrides](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. | | [ImageValidation](./kibana-plugin-core-public.imagevalidation.md) | | | [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | -| [LegacyApp](./kibana-plugin-core-public.legacyapp.md) | | -| [LegacyCoreSetup](./kibana-plugin-core-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the ui/new_platform module. | -| [LegacyCoreStart](./kibana-plugin-core-public.legacycorestart.md) | Start interface exposed to the legacy platform via the ui/new_platform module. | -| [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) | | | [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) | Options for the [navigateToApp API](./kibana-plugin-core-public.applicationstart.navigatetoapp.md) | | [NotificationsSetup](./kibana-plugin-core-public.notificationssetup.md) | | | [NotificationsStart](./kibana-plugin-core-public.notificationsstart.md) | | @@ -173,7 +168,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | | [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) | Public information about a registered [application](./kibana-plugin-core-public.app.md) | -| [PublicLegacyAppInfo](./kibana-plugin-core-public.publiclegacyappinfo.md) | Information about a registered [legacy application](./kibana-plugin-core-public.legacyapp.md) | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.md b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.md index aa51e5706e3d7..b7c01fae4314f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.md @@ -16,7 +16,7 @@ export interface NavigateToAppOptions | Property | Type | Description | | --- | --- | --- | -| [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) | string | optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.appbase.defaultpath.md)\` as default. | +| [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) | string | optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md)\` as default. | | [replace](./kibana-plugin-core-public.navigatetoappoptions.replace.md) | boolean | if true, will not create a new history entry when navigating (using replace instead of push) | | [state](./kibana-plugin-core-public.navigatetoappoptions.state.md) | unknown | optional state to forward to the application | diff --git a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.path.md b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.path.md index 58ce7e02d8dd8..095553d05778c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.path.md +++ b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.path.md @@ -4,7 +4,7 @@ ## NavigateToAppOptions.path property -optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.appbase.defaultpath.md)\` as default. +optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md)\` as default. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.replace.md b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.replace.md index 9530d03486299..8a7440025aedc 100644 --- a/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.replace.md +++ b/docs/development/core/public/kibana-plugin-core-public.navigatetoappoptions.replace.md @@ -11,8 +11,3 @@ if true, will not create a new history entry when navigating (using `replace` in ```typescript replace?: boolean; ``` - -## Remarks - -This option not be used when navigating from and/or to legacy applications. - diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md index 4b3b103c92731..3717dc847db25 100644 --- a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md +++ b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md @@ -10,7 +10,6 @@ Public information about a registered [application](./kibana-plugin-core-public. ```typescript export declare type PublicAppInfo = Omit & { - legacy: false; status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.publiclegacyappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publiclegacyappinfo.md deleted file mode 100644 index 051638daabd12..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.publiclegacyappinfo.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicLegacyAppInfo](./kibana-plugin-core-public.publiclegacyappinfo.md) - -## PublicLegacyAppInfo type - -Information about a registered [legacy application](./kibana-plugin-core-public.legacyapp.md) - -Signature: - -```typescript -export declare type PublicLegacyAppInfo = Omit & { - legacy: true; - status: AppStatus; - navLinkStatus: AppNavLinkStatus; -}; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md deleted file mode 100644 index 7475f0e3a4c1c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) - -## StatusServiceSetup.dependencies$ property - -Current status for all plugins this plugin depends on. Each key of the `Record` is a plugin id. - -Signature: - -```typescript -dependencies$: Observable>; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md deleted file mode 100644 index 6c65e44270a06..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) - -## StatusServiceSetup.derivedStatus$ property - -The status of this plugin as derived from its dependencies. - -Signature: - -```typescript -derivedStatus$: Observable; -``` - -## Remarks - -By default, plugins inherit this derived status from their dependencies. Calling overrides this default status. - -This may emit multliple times for a single status change event as propagates through the dependency tree - diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md index ba0645be4d26c..3d3b73ccda25f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md @@ -12,73 +12,10 @@ API for accessing status of Core and this plugin's dependencies as well as for c export interface StatusServiceSetup ``` -## Remarks - -By default, a plugin inherits it's current status from the most severe status level of any Core services and any plugins that it depends on. This default status is available on the API. - -Plugins may customize their status calculation by calling the API with an Observable. Within this Observable, a plugin may choose to only depend on the status of some of its dependencies, to ignore severe status levels of particular Core services they are not concerned with, or to make its status dependent on other external services. - -## Example 1 - -Customize a plugin's status to only depend on the status of SavedObjects: - -```ts -core.status.set( - core.status.core$.pipe( -. map((coreStatus) => { - return coreStatus.savedObjects; - }) ; - ); -); - -``` - -## Example 2 - -Customize a plugin's status to include an external service: - -```ts -const externalStatus$ = interval(1000).pipe( - switchMap(async () => { - const resp = await fetch(`https://myexternaldep.com/_healthz`); - const body = await resp.json(); - if (body.ok) { - return of({ level: ServiceStatusLevels.available, summary: 'External Service is up'}); - } else { - return of({ level: ServiceStatusLevels.available, summary: 'External Service is unavailable'}); - } - }), - catchError((error) => { - of({ level: ServiceStatusLevels.unavailable, summary: `External Service is down`, meta: { error }}) - }) -); - -core.status.set( - combineLatest([core.status.derivedStatus$, externalStatus$]).pipe( - map(([derivedStatus, externalStatus]) => { - if (externalStatus.level > derivedStatus) { - return externalStatus; - } else { - return derivedStatus; - } - }) - ) -); - -``` - ## Properties | Property | Type | Description | | --- | --- | --- | | [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | Observable<CoreStatus> | Current status for all Core services. | -| [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) | Observable<Record<string, ServiceStatus>> | Current status for all plugins this plugin depends on. Each key of the Record is a plugin id. | -| [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) | Observable<ServiceStatus> | The status of this plugin as derived from its dependencies. | | [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) | Observable<ServiceStatus> | Overall system status for all of Kibana. | -## Methods - -| Method | Description | -| --- | --- | -| [set(status$)](./kibana-plugin-core-server.statusservicesetup.set.md) | Allows a plugin to specify a custom status dependent on its own criteria. Completely overrides the default inherited status. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md deleted file mode 100644 index 143cd397c40ae..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md +++ /dev/null @@ -1,28 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [set](./kibana-plugin-core-server.statusservicesetup.set.md) - -## StatusServiceSetup.set() method - -Allows a plugin to specify a custom status dependent on its own criteria. Completely overrides the default inherited status. - -Signature: - -```typescript -set(status$: Observable): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| status$ | Observable<ServiceStatus> | | - -Returns: - -`void` - -## Remarks - -See the [StatusServiceSetup.derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) API for leveraging the default status calculation that is provided by Core. - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fetchoptions.abortsignal.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fetchoptions.abortsignal.md deleted file mode 100644 index 791f1b63e6539..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fetchoptions.abortsignal.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FetchOptions](./kibana-plugin-plugins-data-public.fetchoptions.md) > [abortSignal](./kibana-plugin-plugins-data-public.fetchoptions.abortsignal.md) - -## FetchOptions.abortSignal property - -Signature: - -```typescript -abortSignal?: AbortSignal; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fetchoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fetchoptions.md deleted file mode 100644 index f07fdd4280533..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fetchoptions.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FetchOptions](./kibana-plugin-plugins-data-public.fetchoptions.md) - -## FetchOptions interface - -Signature: - -```typescript -export interface FetchOptions -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [abortSignal](./kibana-plugin-plugins-data-public.fetchoptions.abortsignal.md) | AbortSignal | | -| [searchStrategyId](./kibana-plugin-plugins-data-public.fetchoptions.searchstrategyid.md) | string | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fetchoptions.searchstrategyid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fetchoptions.searchstrategyid.md deleted file mode 100644 index 8824529eb4eca..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fetchoptions.searchstrategyid.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FetchOptions](./kibana-plugin-plugins-data-public.fetchoptions.md) > [searchStrategyId](./kibana-plugin-plugins-data-public.fetchoptions.searchstrategyid.md) - -## FetchOptions.searchStrategyId property - -Signature: - -```typescript -searchStrategyId?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist._constructor_.md deleted file mode 100644 index 9f9613a5a68f7..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist._constructor_.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [(constructor)](./kibana-plugin-plugins-data-public.fieldlist._constructor_.md) - -## FieldList.(constructor) - -Constructs a new instance of the `FieldList` class - -Signature: - -```typescript -constructor(indexPattern: IndexPattern, specs?: FieldSpec[], shortDotsEnable?: boolean, onNotification?: OnNotification); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| indexPattern | IndexPattern | | -| specs | FieldSpec[] | | -| shortDotsEnable | boolean | | -| onNotification | OnNotification | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.add.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.add.md deleted file mode 100644 index ae3d82f0cc3ea..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.add.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [add](./kibana-plugin-plugins-data-public.fieldlist.add.md) - -## FieldList.add property - -Signature: - -```typescript -readonly add: (field: FieldSpec) => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getall.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getall.md deleted file mode 100644 index da29a4de9acc8..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getall.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [getAll](./kibana-plugin-plugins-data-public.fieldlist.getall.md) - -## FieldList.getAll property - -Signature: - -```typescript -readonly getAll: () => IndexPatternField[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbyname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbyname.md deleted file mode 100644 index af368d003423a..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbyname.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [getByName](./kibana-plugin-plugins-data-public.fieldlist.getbyname.md) - -## FieldList.getByName property - -Signature: - -```typescript -readonly getByName: (name: IndexPatternField['name']) => IndexPatternField | undefined; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbytype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbytype.md deleted file mode 100644 index 16bae3ee7c555..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.getbytype.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [getByType](./kibana-plugin-plugins-data-public.fieldlist.getbytype.md) - -## FieldList.getByType property - -Signature: - -```typescript -readonly getByType: (type: IndexPatternField['type']) => any[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.md index 012b069430290..79bcaf9700cf0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.md @@ -1,32 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [fieldList](./kibana-plugin-plugins-data-public.fieldlist.md) -## FieldList class +## fieldList variable Signature: ```typescript -export declare class FieldList extends Array implements IIndexPatternFieldList +fieldList: (specs?: FieldSpec[], shortDotsEnable?: boolean) => IIndexPatternFieldList ``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(indexPattern, specs, shortDotsEnable, onNotification)](./kibana-plugin-plugins-data-public.fieldlist._constructor_.md) | | Constructs a new instance of the FieldList class | - -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [add](./kibana-plugin-plugins-data-public.fieldlist.add.md) | | (field: FieldSpec) => void | | -| [getAll](./kibana-plugin-plugins-data-public.fieldlist.getall.md) | | () => IndexPatternField[] | | -| [getByName](./kibana-plugin-plugins-data-public.fieldlist.getbyname.md) | | (name: IndexPatternField['name']) => IndexPatternField | undefined | | -| [getByType](./kibana-plugin-plugins-data-public.fieldlist.getbytype.md) | | (type: IndexPatternField['type']) => any[] | | -| [remove](./kibana-plugin-plugins-data-public.fieldlist.remove.md) | | (field: IFieldType) => void | | -| [removeAll](./kibana-plugin-plugins-data-public.fieldlist.removeall.md) | | () => void | | -| [replaceAll](./kibana-plugin-plugins-data-public.fieldlist.replaceall.md) | | (specs: FieldSpec[]) => void | | -| [toSpec](./kibana-plugin-plugins-data-public.fieldlist.tospec.md) | | () => {
count: number;
script: string | undefined;
lang: string | undefined;
conflictDescriptions: Record<string, string[]> | undefined;
name: string;
type: string;
esTypes: string[] | undefined;
scripted: boolean;
searchable: boolean;
aggregatable: boolean;
readFromDocValues: boolean;
subType: import("../types").IFieldSubType | undefined;
format: any;
}[] | | -| [update](./kibana-plugin-plugins-data-public.fieldlist.update.md) | | (field: FieldSpec) => void | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.remove.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.remove.md deleted file mode 100644 index 149410adb3550..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.remove.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [remove](./kibana-plugin-plugins-data-public.fieldlist.remove.md) - -## FieldList.remove property - -Signature: - -```typescript -readonly remove: (field: IFieldType) => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.removeall.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.removeall.md deleted file mode 100644 index 92a45349ad005..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.removeall.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [removeAll](./kibana-plugin-plugins-data-public.fieldlist.removeall.md) - -## FieldList.removeAll property - -Signature: - -```typescript -readonly removeAll: () => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.replaceall.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.replaceall.md deleted file mode 100644 index 5330440e6b96a..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.replaceall.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [replaceAll](./kibana-plugin-plugins-data-public.fieldlist.replaceall.md) - -## FieldList.replaceAll property - -Signature: - -```typescript -readonly replaceAll: (specs: FieldSpec[]) => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.tospec.md deleted file mode 100644 index e646339feb495..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.tospec.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [toSpec](./kibana-plugin-plugins-data-public.fieldlist.tospec.md) - -## FieldList.toSpec property - -Signature: - -```typescript -readonly toSpec: () => { - count: number; - script: string | undefined; - lang: string | undefined; - conflictDescriptions: Record | undefined; - name: string; - type: string; - esTypes: string[] | undefined; - scripted: boolean; - searchable: boolean; - aggregatable: boolean; - readFromDocValues: boolean; - subType: import("../types").IFieldSubType | undefined; - format: any; - }[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.update.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.update.md deleted file mode 100644 index c718e47b31b50..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldlist.update.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) > [update](./kibana-plugin-plugins-data-public.fieldlist.update.md) - -## FieldList.update property - -Signature: - -```typescript -readonly update: (field: FieldSpec) => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md index 6f42fb32fdb7b..3ff2afafcc514 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md @@ -28,7 +28,7 @@ export interface IFieldType | [searchable](./kibana-plugin-plugins-data-public.ifieldtype.searchable.md) | boolean | | | [sortable](./kibana-plugin-plugins-data-public.ifieldtype.sortable.md) | boolean | | | [subType](./kibana-plugin-plugins-data-public.ifieldtype.subtype.md) | IFieldSubType | | -| [toSpec](./kibana-plugin-plugins-data-public.ifieldtype.tospec.md) | () => FieldSpec | | +| [toSpec](./kibana-plugin-plugins-data-public.ifieldtype.tospec.md) | (options?: {
getFormatterForField?: IndexPattern['getFormatterForField'];
}) => FieldSpec | | | [type](./kibana-plugin-plugins-data-public.ifieldtype.type.md) | string | | | [visualizable](./kibana-plugin-plugins-data-public.ifieldtype.visualizable.md) | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md index 1fb4084c25d34..52238ea2a00ca 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md @@ -7,5 +7,7 @@ Signature: ```typescript -toSpec?: () => FieldSpec; +toSpec?: (options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }) => FieldSpec; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.md index b068c4804c0dd..b1e13ffaabd07 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.md @@ -21,5 +21,6 @@ export interface IIndexPatternFieldList extends Array | [remove(field)](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.remove.md) | | | [removeAll()](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.removeall.md) | | | [replaceAll(specs)](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.replaceall.md) | | +| [toSpec(options)](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md) | | | [update(field)](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.update.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md new file mode 100644 index 0000000000000..fd20f2944c5be --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPatternFieldList](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.md) > [toSpec](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md) + +## IIndexPatternFieldList.toSpec() method + +Signature: + +```typescript +toSpec(options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }): FieldSpec[]; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | {
getFormatterForField?: IndexPattern['getFormatterForField'];
} | | + +Returns: + +`FieldSpec[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 37db063e284ec..4c53af3f8970e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -61,7 +61,5 @@ export declare class IndexPattern implements IIndexPattern | [refreshFields()](./kibana-plugin-plugins-data-public.indexpattern.refreshfields.md) | | | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | | | [save(saveAttempts)](./kibana-plugin-plugins-data-public.indexpattern.save.md) | | | -| [toJSON()](./kibana-plugin-plugins-data-public.indexpattern.tojson.md) | | | | [toSpec()](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) | | | -| [toString()](./kibana-plugin-plugins-data-public.indexpattern.tostring.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tojson.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tojson.md deleted file mode 100644 index 0ae04bb424d44..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tojson.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [toJSON](./kibana-plugin-plugins-data-public.indexpattern.tojson.md) - -## IndexPattern.toJSON() method - -Signature: - -```typescript -toJSON(): string | undefined; -``` -Returns: - -`string | undefined` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tostring.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tostring.md deleted file mode 100644 index a10b549a7b9eb..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tostring.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [toString](./kibana-plugin-plugins-data-public.indexpattern.tostring.md) - -## IndexPattern.toString() method - -Signature: - -```typescript -toString(): string; -``` -Returns: - -`string` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md index 10b65bdccdf87..5d467a7a9cbce 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md @@ -9,15 +9,13 @@ Constructs a new instance of the `IndexPatternField` class Signature: ```typescript -constructor(indexPattern: IndexPattern, spec: FieldSpec, displayName: string, onNotification: OnNotification); +constructor(spec: FieldSpec, displayName: string); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| indexPattern | IndexPattern | | | spec | FieldSpec | | | displayName | string | | -| onNotification | OnNotification | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md deleted file mode 100644 index f28d5b1bca7e5..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [format](./kibana-plugin-plugins-data-public.indexpatternfield.format.md) - -## IndexPatternField.format property - -Signature: - -```typescript -get format(): FieldFormat; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md deleted file mode 100644 index 3d145cce9d07d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) - -## IndexPatternField.indexPattern property - -Signature: - -```typescript -readonly indexPattern: IndexPattern; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index 713b29ea3a3d3..215188ffa2607 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -14,7 +14,7 @@ export declare class IndexPatternField implements IFieldType | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(indexPattern, spec, displayName, onNotification)](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) | | Constructs a new instance of the IndexPatternField class | +| [(constructor)(spec, displayName)](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) | | Constructs a new instance of the IndexPatternField class | ## Properties @@ -26,8 +26,6 @@ export declare class IndexPatternField implements IFieldType | [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | undefined | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | -| [format](./kibana-plugin-plugins-data-public.indexpatternfield.format.md) | | FieldFormat | | -| [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) | | IndexPattern | | | [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | undefined | | | [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) | | string | | | [readFromDocValues](./kibana-plugin-plugins-data-public.indexpatternfield.readfromdocvalues.md) | | boolean | | @@ -45,5 +43,5 @@ export declare class IndexPatternField implements IFieldType | Method | Modifiers | Description | | --- | --- | --- | | [toJSON()](./kibana-plugin-plugins-data-public.indexpatternfield.tojson.md) | | | -| [toSpec()](./kibana-plugin-plugins-data-public.indexpatternfield.tospec.md) | | | +| [toSpec({ getFormatterForField, })](./kibana-plugin-plugins-data-public.indexpatternfield.tospec.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md index 5037cb0049e82..1d80c90991f55 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md @@ -7,7 +7,9 @@ Signature: ```typescript -toSpec(): { +toSpec({ getFormatterForField, }?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }): { count: number; script: string | undefined; lang: string | undefined; @@ -20,9 +22,19 @@ toSpec(): { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; - format: any; + format: { + id: any; + params: any; + } | undefined; }; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { getFormatterForField, } | {
getFormatterForField?: IndexPattern['getFormatterForField'];
} | | + Returns: `{ @@ -38,6 +50,9 @@ toSpec(): { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; - format: any; + format: { + id: any; + params: any; + } | undefined; }` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md new file mode 100644 index 0000000000000..fd8d322d54b26 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) + +## ISearchOptions.abortSignal property + +An `AbortSignal` that allows the caller of `search` to abort a search request. + +Signature: + +```typescript +abortSignal?: AbortSignal; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md index 3eb38dc7d52e0..c9018b0048aa3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md @@ -14,6 +14,6 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | -| [signal](./kibana-plugin-plugins-data-public.isearchoptions.signal.md) | AbortSignal | | -| [strategy](./kibana-plugin-plugins-data-public.isearchoptions.strategy.md) | string | | +| [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [strategy](./kibana-plugin-plugins-data-public.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.signal.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.signal.md deleted file mode 100644 index 10bd186d55baa..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.signal.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [signal](./kibana-plugin-plugins-data-public.isearchoptions.signal.md) - -## ISearchOptions.signal property - -Signature: - -```typescript -signal?: AbortSignal; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.strategy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.strategy.md index df7e050691a8f..bd2580957f6c1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.strategy.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.strategy.md @@ -4,6 +4,8 @@ ## ISearchOptions.strategy property +Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 09702df4fdb54..b651480a85899 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -10,7 +10,6 @@ | --- | --- | | [AggParamType](./kibana-plugin-plugins-data-public.aggparamtype.md) | | | [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) | | -| [FieldList](./kibana-plugin-plugins-data-public.fieldlist.md) | | | [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) | | | [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) | | | [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) | | @@ -51,7 +50,6 @@ | [DataPublicPluginSetup](./kibana-plugin-plugins-data-public.datapublicpluginsetup.md) | | | [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | | | [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) | | -| [FetchOptions](./kibana-plugin-plugins-data-public.fetchoptions.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-public.fieldformatconfig.md) | | | [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) | | | [Filter](./kibana-plugin-plugins-data-public.filter.md) | | @@ -103,6 +101,7 @@ | [expandShorthand](./kibana-plugin-plugins-data-public.expandshorthand.md) | | | [extractSearchSourceReferences](./kibana-plugin-plugins-data-public.extractsearchsourcereferences.md) | | | [fieldFormats](./kibana-plugin-plugins-data-public.fieldformats.md) | | +| [fieldList](./kibana-plugin-plugins-data-public.fieldlist.md) | | | [FilterBar](./kibana-plugin-plugins-data-public.filterbar.md) | | | [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {Array} | | [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md index 77a2954428f8d..d106f3a35a91c 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md @@ -28,7 +28,7 @@ export interface IFieldType | [searchable](./kibana-plugin-plugins-data-server.ifieldtype.searchable.md) | boolean | | | [sortable](./kibana-plugin-plugins-data-server.ifieldtype.sortable.md) | boolean | | | [subType](./kibana-plugin-plugins-data-server.ifieldtype.subtype.md) | IFieldSubType | | -| [toSpec](./kibana-plugin-plugins-data-server.ifieldtype.tospec.md) | () => FieldSpec | | +| [toSpec](./kibana-plugin-plugins-data-server.ifieldtype.tospec.md) | (options?: {
getFormatterForField?: IndexPattern['getFormatterForField'];
}) => FieldSpec | | | [type](./kibana-plugin-plugins-data-server.ifieldtype.type.md) | string | | | [visualizable](./kibana-plugin-plugins-data-server.ifieldtype.visualizable.md) | boolean | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md index d1863bebce4f0..6f8ee9d9eebf0 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md @@ -7,5 +7,7 @@ Signature: ```typescript -toSpec?: () => FieldSpec; +toSpec?: (options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }) => FieldSpec; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.signal.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md similarity index 62% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.signal.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md index 948dfd66da7a0..693345f480a9a 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.signal.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [signal](./kibana-plugin-plugins-data-server.isearchoptions.signal.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) -## ISearchOptions.signal property +## ISearchOptions.abortSignal property An `AbortSignal` that allows the caller of `search` to abort a search request. Signature: ```typescript -signal?: AbortSignal; +abortSignal?: AbortSignal; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md index 002ce864a1aa4..21ddaef3a0b94 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md @@ -14,6 +14,6 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | -| [signal](./kibana-plugin-plugins-data-server.isearchoptions.signal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | -| [strategy](./kibana-plugin-plugins-data-server.isearchoptions.strategy.md) | string | | +| [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [strategy](./kibana-plugin-plugins-data-server.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.strategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.strategy.md index 6df72d023e2c0..65da7fddd13f6 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.strategy.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.strategy.md @@ -4,6 +4,8 @@ ## ISearchOptions.strategy property +Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. + Signature: ```typescript diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index eef2a12a964b8..da58382deb89a 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -1,7 +1,7 @@ [[search]] == Search data Many Kibana apps embed a query bar for real-time search, including -*Discover*, *Visualize*, and *Dashboard*. +*Discover* and *Dashboard*. [float] === Search your data @@ -84,7 +84,7 @@ query language you can also submit queries using the {ref}/query-dsl.html[Elasti [[save-open-search]] === Save a search -A saved search persists your current view of Discover for later retrieval and reuse. You can reload a saved search into Discover, add it to a dashboard, and use it as the basis for a <>. +A saved search persists your current view of Discover for later retrieval and reuse. You can reload a saved search into Discover, add it to a dashboard, and use it as the basis for a visualization. A saved search includes the query text, filters, and optionally, the time filter. A saved search also includes the selected columns in the document table, the sort order, and the current index pattern. @@ -120,7 +120,7 @@ used for the saved search will also be automatically selected. [[save-load-delete-query]] === Save a query -A saved query is a portable collection of query text and filters that you can reuse in <>, <>, and <>. Save a query when you want to: +A saved query is a portable collection of query text and filters that you can reuse in <> and <>. Save a query when you want to: * Retrieve results from the same query at a later time without having to reenter the query text, add the filters or set the time filter * View the results of the same query in multiple apps @@ -148,7 +148,7 @@ image::discover/images/saved-query-save-form-default-filters.png["Example of the . Click *Save*. ==== Load a query -To load a saved query into Discover, Dashboard, or Visualize: +To load a saved query into Discover or Dashboard: . Click *#* in the search bar, next to the query text input. . Select the query you want to load. You might need to scroll down to find the query you are looking for. diff --git a/docs/drilldowns/explore-underlying-data.asciidoc b/docs/drilldowns/explore-underlying-data.asciidoc deleted file mode 100644 index c2bba599730d8..0000000000000 --- a/docs/drilldowns/explore-underlying-data.asciidoc +++ /dev/null @@ -1,41 +0,0 @@ -[[explore-underlying-data]] -== Explore the underlying data for a visualization - -++++ -Explore the underlying data -++++ - -Dashboard panels have an *Explore underlying data* action that navigates you to *Discover*, -where you can narrow your documents to the ones you'll most likely use in a visualization. -This action is available for visualizations backed by a single index pattern. - -You can access *Explore underlying data* in two ways: from the panel context -menu or from the menu that appears when you interact with the chart. - -[float] -[[explore-data-from-panel-context-menu]] -=== Explore data from panel context menu - -The *Explore underlying data* action in the panel menu navigates you to Discover, -carrying over the index pattern, filters, query, and time range for the visualization. - -[role="screenshot"] -image::images/explore_data_context_menu.png[Explore underlying data from panel context menu] - -[float] -[[explore-data-from-chart]] -=== Explore data from chart action - -Initiating *Explore underlying data* from the chart also navigates to Discover, -carrying over the current context for the visualization. In addition, this action -applies the filters and time range created by the events that triggered the action. - -[role="screenshot"] -image::images/explore_data_in_chart.png[Explore underlying data from chart] - -To enable this action add the following line to your `kibana.yml` config. - -["source","yml"] ------------ -xpack.discoverEnhanced.actions.exploreDataInChart.enabled: true ------------ diff --git a/docs/getting-started/add-sample-data.asciidoc b/docs/getting-started/add-sample-data.asciidoc deleted file mode 100644 index ab43431601888..0000000000000 --- a/docs/getting-started/add-sample-data.asciidoc +++ /dev/null @@ -1,28 +0,0 @@ -[[add-sample-data]] -== Add sample data - -{kib} has several sample data sets that you can use to explore {kib} before loading your own data. -These sample data sets showcase a variety of use cases: - -* *eCommerce orders* includes visualizations for product-related information, -such as cost, revenue, and price. -* *Flight data* enables you to view and interact with flight routes. -* *Web logs* lets you analyze website traffic. - -To get started, go to the {kib} home page and click the link underneath *Add sample data*. - -Once you've loaded a data set, click *View data* to view prepackaged -visualizations, dashboards, Canvas workpads, Maps, and Machine Learning jobs. - -[role="screenshot"] -image::images/add-sample-data.png[] - -NOTE: The timestamps in the sample data sets are relative to when they are installed. -If you uninstall and reinstall a data set, the timestamps will change to reflect the most recent installation. - -[float] -=== Next steps - -* Explore {kib} by following the <>. - -* Learn how to load data, define index patterns, and build visualizations by <>. diff --git a/docs/getting-started/images/gs_maps_time_filter.png b/docs/getting-started/images/gs_maps_time_filter.png new file mode 100644 index 0000000000000..83e20c279906e Binary files /dev/null and b/docs/getting-started/images/gs_maps_time_filter.png differ diff --git a/docs/getting-started/images/tutorial-discover-2.png b/docs/getting-started/images/tutorial-discover-2.png index 7190c90d8e5ba..681e4834de830 100644 Binary files a/docs/getting-started/images/tutorial-discover-2.png and b/docs/getting-started/images/tutorial-discover-2.png differ diff --git a/docs/getting-started/images/tutorial-pattern-1.png b/docs/getting-started/images/tutorial-pattern-1.png index 8a289f93fc66e..0026b18775518 100644 Binary files a/docs/getting-started/images/tutorial-pattern-1.png and b/docs/getting-started/images/tutorial-pattern-1.png differ diff --git a/docs/getting-started/images/tutorial-visualize-bar-1.5.png b/docs/getting-started/images/tutorial-visualize-bar-1.5.png index c02b9ca59dff5..009152f9407e4 100644 Binary files a/docs/getting-started/images/tutorial-visualize-bar-1.5.png and b/docs/getting-started/images/tutorial-visualize-bar-1.5.png differ diff --git a/docs/getting-started/images/tutorial-visualize-map-2.png b/docs/getting-started/images/tutorial-visualize-map-2.png index f4d1d0e47fe6a..ed2fd47cb27de 100644 Binary files a/docs/getting-started/images/tutorial-visualize-map-2.png and b/docs/getting-started/images/tutorial-visualize-map-2.png differ diff --git a/docs/getting-started/images/tutorial-visualize-md-2.png b/docs/getting-started/images/tutorial-visualize-md-2.png index 9e9a670ba196f..af56faa3b0516 100644 Binary files a/docs/getting-started/images/tutorial-visualize-md-2.png and b/docs/getting-started/images/tutorial-visualize-md-2.png differ diff --git a/docs/getting-started/images/tutorial-visualize-pie-2.png b/docs/getting-started/images/tutorial-visualize-pie-2.png index ef5d62b4ceee7..ca8f5e92146bc 100644 Binary files a/docs/getting-started/images/tutorial-visualize-pie-2.png and b/docs/getting-started/images/tutorial-visualize-pie-2.png differ diff --git a/docs/getting-started/images/tutorial-visualize-pie-3.png b/docs/getting-started/images/tutorial-visualize-pie-3.png index 6974c8d34b0dd..59fce360096c0 100644 Binary files a/docs/getting-started/images/tutorial-visualize-pie-3.png and b/docs/getting-started/images/tutorial-visualize-pie-3.png differ diff --git a/docs/getting-started/images/tutorial_index_patterns.png b/docs/getting-started/images/tutorial_index_patterns.png new file mode 100644 index 0000000000000..430baf898b612 Binary files /dev/null and b/docs/getting-started/images/tutorial_index_patterns.png differ diff --git a/docs/getting-started/tutorial-dashboard.asciidoc b/docs/getting-started/tutorial-dashboard.asciidoc deleted file mode 100644 index 2ee2d76024aed..0000000000000 --- a/docs/getting-started/tutorial-dashboard.asciidoc +++ /dev/null @@ -1,53 +0,0 @@ -[[tutorial-dashboard]] -=== Add the visualizations to a dashboard - -Build a dashboard that contains the visualizations and map that you saved during -this tutorial. - -. Open the menu, go to *Dashboard*, then click *Create dashboard*. -. Set the time filter to May 18, 2015 to May 20, 2015. -. Click *Add*, then select the following: - * *Bar Example* - * *Map Example* - * *Markdown Example* - * *Pie Example* -+ -Your sample dashboard looks like this: -+ -[role="screenshot"] -image::images/tutorial-dashboard.png[] - -. Try out the editing controls. -+ -You can rearrange the visualizations by clicking a the header of a -visualization and dragging. The gear icon in the top right of a visualization -displays controls for editing and deleting the visualization. A resize control -is on the lower right. - -. *Save* your dashboard. - -==== Inspect the data - -Seeing visualizations of your data is great, -but sometimes you need to look at the actual data to -understand what's really going on. You can inspect the data behind any visualization -and view the {es} query used to retrieve it. - -. Click the pie chart *Options* menu, then select *Inspect*. -+ -[role="screenshot"] -image::images/tutorial-full-inspect1.png[] - -. To look at the query used to fetch the data for the visualization, select *View > Requests*. - -[float] -=== Next steps - -Now that you have the basics, you're ready to start exploring -your own data with {kib}. - -* To learn about searching and filtering your data, refer to {kibana-ref}/discover.html[Discover]. -* To learn about the visualization types {kib} has to offer, refer to {kibana-ref}/visualize.html[Visualize]. -* To learn about configuring {kib} and managing your saved objects, refer to {kibana-ref}/management.html[Management]. -* To learn about the interactive console you can use to submit REST requests to {es}, refer to {kibana-ref}/console-kibana.html[Console]. - diff --git a/docs/getting-started/tutorial-define-index.asciidoc b/docs/getting-started/tutorial-define-index.asciidoc index 254befa55faea..fbe7450683dbc 100644 --- a/docs/getting-started/tutorial-define-index.asciidoc +++ b/docs/getting-started/tutorial-define-index.asciidoc @@ -1,7 +1,7 @@ [[tutorial-define-index]] === Define your index patterns -Index patterns tell Kibana which Elasticsearch indices you want to explore. +Index patterns tell {kib} which {es} indices you want to explore. An index pattern can match the name of a single index, or include a wildcard (*) to match multiple indices. @@ -10,28 +10,29 @@ series of indices in the format `logstash-YYYY.MMM.DD`. To explore all of the log data from May 2018, you could specify the index pattern `logstash-2018.05*`. - [float] -==== Create your first index pattern +==== Create the index patterns First you'll create index patterns for the Shakespeare data set, which has an index named `shakespeare,` and the accounts data set, which has an index named `bank`. These data sets don't contain time series data. . Open the menu, then go to *Stack Management > {kib} > Index Patterns*. + . If this is your first index pattern, the *Create index pattern* page opens. -Otherwise, click *Create index pattern*. -. In the *Index pattern field*, enter `shakes*`. + +. In the *Index pattern name* field, enter `shakes*`. + [role="screenshot"] -image::images/tutorial-pattern-1.png[] +image::images/tutorial-pattern-1.png[shakes* index patterns] . Click *Next step*. -. Select the *Time Filter field name*, then click *Create index pattern*. + +. On the *Configure settings* page, *Create index pattern*. + You’re presented a table of all fields and associated data types in the index. -. Return to the *Index patterns* page and create a second index pattern named `ba*`. +. Create a second index pattern named `ba*`. [float] ==== Create an index pattern for the time series data @@ -39,15 +40,12 @@ You’re presented a table of all fields and associated data types in the index. Create an index pattern for the Logstash index, which contains the time series data. -. Define an index pattern named `logstash*`. -. Click *Next step*. -. From the *Time Filter field name* dropdown, select *@timestamp*. -. Click *Create index pattern*. +. Create an index pattern named `logstash*`, then click *Next step*. -NOTE: When you define an index pattern, the indices that match that pattern must -exist in Elasticsearch and they must contain data. To check which indices are -available, open the menu, then go to *Dev Tools > Console* and enter `GET _cat/indices`. Alternately, use -`curl -XGET "http://localhost:9200/_cat/indices"`. +. From the *Time field* dropdown, select *@timestamp, then click *Create index pattern*. ++ +[role="screenshot"] +image::images/tutorial_index_patterns.png[All tutorial index patterns] diff --git a/docs/getting-started/tutorial-discovering.asciidoc b/docs/getting-started/tutorial-discovering.asciidoc index 31d77be1275ee..ec07a74b8ac0d 100644 --- a/docs/getting-started/tutorial-discovering.asciidoc +++ b/docs/getting-started/tutorial-discovering.asciidoc @@ -1,9 +1,8 @@ -[[tutorial-discovering]] -=== Discover your data +[[explore-your-data]] +=== Explore your data -Using *Discover*, enter -an {ref}/query-dsl-query-string-query.html#query-string-syntax[Elasticsearch -query] to search your data and filter the results. +With *Discover*, you use {ref}/query-dsl-query-string-query.html#query-string-syntax[Elasticsearch +queries] to explore your data and narrow the results with filters. . Open the menu, then go to *Discover*. + @@ -13,7 +12,7 @@ The `shakes*` index pattern appears. + By default, all fields are shown for each matching document. -. In the *Search* field, enter the following: +. In the *Search* field, enter the following, then click *Update*: + [source,text] account_number<100 AND balance>47500 @@ -32,3 +31,5 @@ account numbers. + [role="screenshot"] image::images/tutorial-discover-3.png[] + +Now that you know what your documents contain, it's time to gain insight into your data with visualizations. diff --git a/docs/getting-started/tutorial-full-experience.asciidoc b/docs/getting-started/tutorial-full-experience.asciidoc index e6f2de87905bf..1e6fe39dbd013 100644 --- a/docs/getting-started/tutorial-full-experience.asciidoc +++ b/docs/getting-started/tutorial-full-experience.asciidoc @@ -1,32 +1,23 @@ -[[tutorial-build-dashboard]] -== Build your own dashboard +[[create-your-own-dashboard]] +== Create your own dashboard -Want to load some data into Kibana and build a dashboard? This tutorial shows you how to: +Ready to add data to {kib} and create your own dashboard? In this tutorial, you'll use three types of data sets that'll help you learn to: -* <> -* <> -* <> -* <> -* <> - -When you complete this tutorial, you'll have a dashboard that looks like this. - -[role="screenshot"] -image::images/tutorial-dashboard.png[] +* <> +* <> +* <> +* <> [float] -[[tutorial-load-dataset]] -=== Load sample data +[[download-the-data]] +=== Download the data -This tutorial requires you to download three data sets: +To complete the tutorial, you'll download and use the following data sets: * The complete works of William Shakespeare, suitably parsed into fields -* A set of fictitious accounts with randomly generated data +* A set of fictitious bank accounts with randomly generated data * A set of randomly generated log files -[float] -==== Download the data sets - Create a new working directory where you want to download the files. From that directory, run the following commands: [source,shell] @@ -34,7 +25,7 @@ curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/shakespeare. curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/accounts.zip curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/logs.jsonl.gz -Two of the data sets are compressed. To extract the files, use these commands: +Two of the data sets are compressed. To extract the files, use the following commands: [source,shell] unzip accounts.zip @@ -43,7 +34,7 @@ gunzip logs.jsonl.gz [float] ==== Structure of the data sets -The Shakespeare data set has this structure: +The Shakespeare data set has the following structure: [source,json] { @@ -55,7 +46,7 @@ The Shakespeare data set has this structure: "text_entry": "String", } -The accounts data set is structured as follows: +The accounts data set has the following structure: [source,json] { @@ -72,7 +63,7 @@ The accounts data set is structured as follows: "state": "String" } -The logs data set has dozens of different fields. Here are the notable fields for this tutorial: +The logs data set has dozens of different fields. The notable fields include the following: [source,json] { @@ -94,7 +85,7 @@ You must also have the `create`, `manage` `read`, `write,` and `delete` index privileges. See {ref}/security-privileges.html[Security privileges] for more information. -Open *Dev Tools*. On the *Console* page, set up a mapping for the Shakespeare data set: +Open the menu, then go to *Dev Tools*. On the *Console* page, set up a mapping for the Shakespeare data set: [source,js] PUT /shakespeare @@ -111,10 +102,11 @@ PUT /shakespeare //CONSOLE -This mapping specifies field characteristics for the data set: +The mapping specifies field characteristics for the data set: * The `speaker` and `play_name` fields are keyword fields. These fields are not analyzed. The strings are treated as a single unit even if they contain multiple words. + * The `line_id` and `speech_number` fields are integers. The logs data set requires a mapping to label the latitude and longitude pairs @@ -177,6 +169,7 @@ PUT /logstash-2015.05.20 The accounts data set doesn't require any mappings. [float] +[[load-the-data-sets]] ==== Load the data sets At this point, you're ready to use the Elasticsearch {ref}/docs-bulk.html[bulk] @@ -195,14 +188,20 @@ Invoke-RestMethod "http://:/_bulk?pretty" -Method Post -ContentType These commands might take some time to execute, depending on the available computing resources. -Verify successful loading: +When you define an index pattern, the indices that match the pattern must +exist in {es} and contain data. + +To verify the availability of the indices, open the menu, go to *Dev Tools > Console*, then enter: [source,js] GET /_cat/indices?v -//CONSOLE +Alternately, use: + +[source,shell] +`curl -XGET "http://localhost:9200/_cat/indices"`. -Your output should look similar to this: +The output should look similar to: [source,shell] health status index pri rep docs.count docs.deleted store.size pri.store.size diff --git a/docs/getting-started/tutorial-sample-data.asciidoc b/docs/getting-started/tutorial-sample-data.asciidoc index 2460a55e13293..18ef862272f85 100644 --- a/docs/getting-started/tutorial-sample-data.asciidoc +++ b/docs/getting-started/tutorial-sample-data.asciidoc @@ -1,207 +1,159 @@ -[[tutorial-sample-data]] +[[explore-kibana-using-sample-data]] == Explore {kib} using sample data -Ready to get some hands-on experience with Kibana? -In this tutorial, you’ll work -with Kibana sample data and learn to: +Ready to get some hands-on experience with {kib}? +In this tutorial, you’ll work with {kib} sample data and learn to: -* <> -* <> -* <> -* <> +* <> +* <> -NOTE: If security is enabled, you must have `read`, `write`, and `manage` privileges -on the `kibana_sample_data_*` indices. See -{ref}/security-privileges.html[Security privileges] for more information. +* <> +NOTE: If security is enabled, you must have `read`, `write`, and `manage` privileges +on the `kibana_sample_data_*` indices. For more information, refer to +{ref}/security-privileges.html[Security privileges]. [float] -=== Add sample data +[[add-the-sample-data]] +=== Add the sample data -Install the Flights sample data set, if you haven't already. +Add the *Sample flight data*. . On the home page, click *Load a data set and a {kib} dashboard*. + . On the *Sample flight data* card, click *Add data*. -. Once the data is added, click *View data > Dashboard*. -+ -You’re taken to the *Global Flight* dashboard, a collection of charts, graphs, -maps, and other visualizations of the the data in the `kibana_sample_data_flights` index. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-dashboard.png[] [float] -[[tutorial-sample-filter]] -=== Filter and query the data +[[explore-the-data]] +=== Explore the data -You can use filters and queries to -narrow the view of the data. -For more detailed information on these actions, see -{ref}/query-filter-context.html[Query and filter context]. +Explore the documents in the index that +match the selected index pattern. The index pattern tells {kib} which {es} index you want to +explore. -[float] -==== Filter the data +. Open the menu, then go to *Discover*. -. In the *Controls* visualization, select an *Origin City* and a *Destination City*. -. Click *Apply changes*. +. Make sure `kibana_sample_data_flights` is the current index pattern. +You might need to click *New* in the {kib} toolbar to refresh the data. + -The `OriginCityName` and the `DestCityName` fields filter the data on the dasbhoard to match -the data you specified. +You'll see a histogram that shows the distribution of +documents over time. A table lists the fields for +each document that matches the index. By default, all fields are shown. + -For example, the following dashboard shows the data for flights from London to Milan. +[role="screenshot"] +image::getting-started/images/tutorial-sample-discover1.png[] + +. Hover over the list of *Available fields*, then click *Add* next +to each field you want explore in the table. + [role="screenshot"] -image::getting-started/images/tutorial-sample-filter.png[] +image::getting-started/images/tutorial-sample-discover2.png[] -. To add a filter manually, click *Add filter*, -then specify the data you want to view. +[float] +[[view-and-analyze-the-data]] +=== View and analyze the data -. When you are finished experimenting, remove all filters. +A _dashboard_ is a collection of panels that provide you with an overview of your data that you can +use to analyze your data. Panels contain everything you need, including visualizations, +interactive controls, Markdown, and more. + +To open the *Global Flight* dashboard, open the menu, then go to *Dashboard*. +[role="screenshot"] +image::getting-started/images/tutorial-sample-dashboard.png[] [float] -[[tutorial-sample-query]] -==== Query the data +[[change-the-panel-data]] +==== Change the panel data -. To find all flights out of Rome, enter this query in the query bar and click *Update*: -+ -[source,text] -OriginCityName:Rome +To gain insights into your data, change the appearance and behavior of the panels. +For example, edit the metric panel to find the airline that has the lowest average fares. -. For a more complex query with AND and OR, try this: -+ -[source,text] -OriginCityName:Rome AND (Carrier:JetBeats OR "Kibana Airlines") -+ -The dashboard updates to show data for the flights out of Rome on JetBeats and -{kib} Airlines. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-query.png[] +. In the {kib} toolbar, click *Edit*. -. When you are finished exploring the dashboard, remove the query by -clearing the contents in the query bar and clicking *Update*. +. In the *Average Ticket Price* metric panel, open the panel menu, then select *Edit visualization*. -[float] -[[tutorial-sample-discover]] -=== Discover the data +. To change the data on the panel, use an {es} {ref}/search-aggregations.html[bucket aggregation], +which sorts the documents that match your search criteria into different categories or buckets. -In Discover, you have access to every document in every index that -matches the selected index pattern. The index pattern tells {kib} which {es} index you are currently -exploring. You can submit search queries, filter the -search results, and view document data. +.. In the *Buckets* pane, select *Add > Split group*. -. From the menu, click *Discover*. +.. From the *Aggregation* dropdown, select *Terms*. -. Ensure `kibana_sample_data_flights` is the current index pattern. -You might need to click *New* in the menu bar to refresh the data. +.. From the *Field* dropdown, select *Carrier*. + +.. Set *Descending* to *4*, then click *Update*. + -You'll see a histogram that shows the distribution of -documents over time. A table lists the fields for -each matching document. By default, all fields are shown. +The average ticket price for all four airlines appear in the visualization builder. + [role="screenshot"] -image::getting-started/images/tutorial-sample-discover1.png[] +image::getting-started/images/tutorial-sample-edit1.png[] -. To choose which fields to display, -hover the pointer over the list of *Available fields*, and then click *add* next -to each field you want include as a column in the table. -+ -For example, if you add the `DestAirportID` and `DestWeather` fields, -the display includes columns for those two fields. +. To save your changes, click *Save and return* in the {kib} toolbar. + +. To save the dashboard, click *Save* in the {kib} toolbar. + [role="screenshot"] -image::getting-started/images/tutorial-sample-discover2.png[] +image::getting-started/images/tutorial-sample-edit2.png[] [float] -[[tutorial-sample-edit]] -=== Edit a visualization - -You have edit permissions for the *Global Flight* dashboard, so you can change -the appearance and behavior of the visualizations. For example, you might want -to see which airline has the lowest average fares. - -. In the side navigation, click *Recently viewed* and open the *Global Flight Dashboard*. -. In the menu bar, click *Edit*. -. In the *Average Ticket Price* visualization, click the gear icon in -the upper right. -. From the *Options* menu, select *Edit visualization*. -+ -*Average Ticket Price* is a metric visualization. -To specify which groups to display -in this visualization, you use an {es} {ref}/search-aggregations.html[bucket aggregation]. -This aggregation sorts the documents that match your search criteria into different -categories, or buckets. +[[filter-and-query-the-data]] +==== Filter and query the data -[float] -==== Create a bucket aggregation +To focus in on the data you want to explore, use filters and queries. +For more information, refer to +{ref}/query-filter-context.html[Query and filter context]. + +To filter the data: -. In the *Buckets* pane, select *Add > Split group*. -. In the *Aggregation* dropdown, select *Terms*. -. In the *Field* dropdown, select *Carrier*. -. Set *Descending* to *4*. -. Click *Apply changes* image:images/apply-changes-button.png[]. +. In the *Controls* visualization, select an *Origin City* and *Destination City*, then click *Apply changes*. + -You now see the average ticket price for all four airlines. +The `OriginCityName` and the `DestCityName` fields filter the data in the panels. + -[role="screenshot"] -image::getting-started/images/tutorial-sample-edit1.png[] - -[float] -==== Save the visualization - -. In the menu bar, click *Save*. -. Leave the visualization name as is and confirm the save. -. Go to the *Global Flight* dashboard and scroll the *Average Ticket Price* visualization to see the four prices. -. Optionally, edit the dashboard. Resize the panel -for the *Average Ticket Price* visualization by dragging the -handle in the lower right. You can also rearrange the visualizations by clicking -the header and dragging. Be sure to save the dashboard. +For example, the following dashboard shows the data for flights from London to Milan. + [role="screenshot"] -image::getting-started/images/tutorial-sample-edit2.png[] +image::getting-started/images/tutorial-sample-filter.png[] -[float] -[[tutorial-sample-inspect]] -=== Inspect the data +. To manually add a filter, click *Add filter*, +then specify the data you want to view. -Seeing visualizations of your data is great, -but sometimes you need to look at the actual data to -understand what's really going on. You can inspect the data behind any visualization -and view the {es} query used to retrieve it. +. When you are finished experimenting, remove all filters. -. In the dashboard, hover the pointer over the pie chart, and then click the icon in the upper right. -. From the *Options* menu, select *Inspect*. +[[query-the-data]] +To query the data: + +. To view all flights out of Rome, enter the following in the *KQL* query bar, then click *Update*: + -The initial view shows the document count. +[source,text] +OriginCityName: Rome + +. For a more complex query with AND and OR, enter: ++ +[source,text] +OriginCityName:Rome AND (Carrier:JetBeats OR Carrier:"Kibana Airlines") ++ +The dashboard panels update to display the flights out of Rome on JetBeats and +{kib} Airlines. + [role="screenshot"] -image::getting-started/images/tutorial-sample-inspect1.png[] - -. To look at the query used to fetch the data for the visualization, select *View > Requests* -in the upper right of the Inspect pane. - -[float] -[[tutorial-sample-remove]] -=== Remove the sample data set -When you’re done experimenting with the sample data set, you can remove it. +image::getting-started/images/tutorial-sample-query.png[] -. Go to the *Sample data* page. -. On the *Sample flight data* card, click *Remove*. +. When you are finished exploring, remove the query by +clearing the contents in the *KQL* query bar, then click *Update*. [float] === Next steps -Now that you have a handle on the {kib} basics, you might be interested in the -tutorial <>, where you'll learn to: +Now that you know the {kib} basics, try out the <> tutorial, where you'll learn to: + +* Add a data set to {kib} -* Load data * Define an index pattern -* Discover and explore data -* Create visualizations -* Add visualizations to a dashboard +* Discover and explore data +* Create and add panels to a dashboard diff --git a/docs/getting-started/tutorial-visualizing.asciidoc b/docs/getting-started/tutorial-visualizing.asciidoc index 20b4e33583072..33a7035160247 100644 --- a/docs/getting-started/tutorial-visualizing.asciidoc +++ b/docs/getting-started/tutorial-visualizing.asciidoc @@ -1,47 +1,76 @@ [[tutorial-visualizing]] === Visualize your data -In *Visualize*, you can shape your data using a variety -of charts, tables, and maps, and more. In this tutorial, you'll create four -visualizations: +Shape your data using a variety +of {kib} supported visualizations, tables, and more. In this tutorial, you'll create four +visualizations that you'll use to create a dashboard. -* <> -* <> -* <> -* <> +To begin, open the menu, go to *Dashboard*, then click *Create new dashboard*. [float] -[[tutorial-visualize-pie]] -=== Pie chart +[[compare-the-number-of-speaking-parts-in-the-play]] +=== Compare the number of speaking parts in the plays -Use the pie chart to -gain insight into the account balances in the bank account data. +To visualize the Shakespeare data and compare the number of speaking parts in the plays, create a bar chart using *Lens*. -. Open then menu, then go to *Visualize*. -. Click *Create visualization*. +. Click *Create new*, then click *Lens* on the *New Visualization* window. + [role="screenshot"] -image::images/tutorial-visualize-wizard-step-1.png[] -. Click *Pie*. +image::images/tutorial-visualize-wizard-step-1.png[Bar chart] -. On the *Choose a source* window, select `ba*`. +. Make sure the index pattern is *shakes*. + +. Display the play data along the x-axis. + +.. From the *Available fields* list, drag and drop *play_name* to the *X-axis* field. + +.. Click *Top values of play_name*. + +.. From the *Order direction* dropdown, select *Ascending*. + +.. In the *Label* field, enter `Play Name`. + +. Display the number of speaking parts per play along the y-axis. + +.. From the *Available fields* list, drag and drop *speaker* to the *Y-axis* field. + +.. Click *Unique count of speaker*. + +.. In the *Label* field, enter `Speaking Parts`. ++ +[role="screenshot"] +image::images/tutorial-visualize-bar-1.5.png[Bar chart] + +. *Save* the chart with the name `Bar Example`. + -Initially, the pie contains a single "slice." -That's because the default search matches all documents. +To show a tooltip with the number of speaking parts for that play, hover over a bar. + -To specify which slices to display in the pie, you use an Elasticsearch -{ref}/search-aggregations.html[bucket aggregation]. This aggregation -sorts the documents that match your search criteria into different -categories. You'll use a bucket aggregation to establish -multiple ranges of account balances and find out how many accounts fall into -each range. +Notice how the individual play names show up as whole phrases, instead of +broken up into individual words. This is the result of the mapping +you did at the beginning of the tutorial, when you marked the `play_name` field +as `not analyzed`. -. In the *Buckets* pane, click *Add > Split slices.* +[float] +[[view-the-average-account-balance-by-age]] +=== View the average account balance by age + +To gain insight into the account balances in the bank account data, create a pie chart. In this tutorial, you'll use the {es} +{ref}/search-aggregations.html[bucket aggregation] to specify the pie slices to display. The bucket aggregation sorts the documents that match your search criteria into different +categories and establishes multiple ranges of account balances so that you can find how many accounts fall into each range. + +. Click *Create new*, then click *Pie* on the *New Visualization* window. + +. On the *Choose a source* window, select `ba*`. + +Since the default search matches all documents, the pie contains a single slice. + +. In the *Buckets* pane, click *Add > Split slices.* + .. From the *Aggregation* dropdown, select *Range*. + .. From the *Field* dropdown, select *balance*. -.. Click *Add range* four times to bring the total number of ranges to six. -.. Define the following ranges: + +.. Click *Add range* until there are six rows of fields, then define the following ranges: + [source,text] 0 999 @@ -53,80 +82,83 @@ each range. . Click *Update*. + -Now you can see what proportion of the 1000 accounts fall into each balance -range. +The pie chart displays the proportion of the 1,000 accounts that fall into each of the ranges. + [role="screenshot"] -image::images/tutorial-visualize-pie-2.png[] +image::images/tutorial-visualize-pie-2.png[Pie chart] -. Add another bucket aggregation that looks at the ages of the account -holders. +. Add another bucket aggregation that displays the ages of the account holders. .. In the *Buckets* pane, click *Add*, then click *Split slices*. + .. From the *Sub aggregation* dropdown, select *Terms*. -.. From the *Field* dropdown, select *age*. -. Click *Update*. +.. From the *Field* dropdown, select *age*, then click *Update*. + The break down of the ages of the account holders are displayed in a ring around the balance ranges. + [role="screenshot"] -image::images/tutorial-visualize-pie-3.png[] +image::images/tutorial-visualize-pie-3.png[Final pie chart] . Click *Save*, then enter `Pie Example` in the *Title* field. [float] -[[tutorial-visualize-bar]] -=== Bar chart +[role="xpack"] +[[visualize-geographic-information]] +=== Visualize geographic information -Use a bar chart to look at the Shakespeare data set and compare -the number of speaking parts in the plays. +To visualize geographic information in the log file data, use <>. -. Click *Create visualization > Vertical Bar*, then set the source to `shakes*`. +. Click *Create new*, then click *Maps* on the *New Visualization* window. + +. To change the time, use the time filter. + +.. Set the *Start date* to `May 18, 2015 @ 12:00:00.000`. + +.. Set the *End date* to `May 20, 2015 @ 12:00:00.000`. + -Initially, the chart is a single bar that shows the total count -of documents that match the default wildcard query. +[role="screenshot"] +image::images/gs_maps_time_filter.png[Time filter for Maps tutorial] -. Show the number of speaking parts per play along the y-axis. +.. Click *Update* + +. Map the geo coordinates from the log files. -.. In the *Metrics* pane, expand *Y-axis*. -.. From the *Aggregation* dropdown, select *Unique Count*. -.. From the *Field* dropdown, select *speaker*. -.. In the *Custom label* field, enter `Speaking Parts`. +.. Click *Add layer > Clusters and grids*. -. Click *Update*. +.. From the *Index pattern* dropdown, select *logstash*. -. Show the plays along the x-axis. +.. Click *Add layer*. -.. In the *Buckets* pane, click *Add > X-axis*. -.. From the *Aggregation* dropdown, select *Terms*. -.. From the *Field* dropdown, select *play_name*. -.. To list the plays alphabetically, select *Ascending* from the *Order* dropdown. -.. In the *Custom label* field, enter `Play Name`. +. Specify the *Layer Style*. -. Click *Update*. +.. From the *Fill color* dropdown, select the yellow to red color ramp. + +.. In the *Border width* field, enter `3`. + +.. From the *Border color* dropdown, select *#FFF*, then click *Save & close*. + [role="screenshot"] -image::images/tutorial-visualize-bar-1.5.png[] -. *Save* the chart with the name `Bar Example`. -+ -Hovering over a bar shows a tooltip with the number of speaking parts for -that play. -+ -Notice how the individual play names show up as whole phrases, instead of -broken into individual words. This is the result of the mapping -you did at the beginning of the tutorial, when you marked the `play_name` field -as `not analyzed`. +image::images/tutorial-visualize-map-2.png[Map] + +. Click *Save*, then enter `Map Example` in the *Title* field. + +. Add the map to your dashboard. + +.. Open the menu, go to *Dashboard*, then click *Add*. + +.. On the *Add panels* flyout, click *Map Example*. [float] [[tutorial-visualize-markdown]] -=== Markdown +=== Add context to your visualizations with Markdown -Add formatted text to your dashboard with a markdown tool. +Add context to your new visualizations with Markdown text. -. Click *Create visualization > Markdown*. -. In the text field, enter the following: +. Click *Create new*, then click *Markdown* on the *New Visualization* window. + +. In the *Markdown* text field, enter: + [source,markdown] # This is a tutorial dashboard! @@ -140,40 +172,22 @@ The Markdown renders in the preview pane. [role="screenshot"] image::images/tutorial-visualize-md-2.png[] -. *Save* the tool with the name `Markdown Example`. +. Click *Save*, then enter `Markdown Example` in the *Title* field. -[float] -[[tutorial-visualize-map]] -=== Map +[role="screenshot"] +image::images/tutorial-dashboard.png[] -Using <>, you can visualize geographic information in the log file sample data. +[float] +=== Next steps -. Click *Create visualization > Maps*. +Now that you have the basics, you're ready to start exploring your own system data with {kib}. -. Set the time. -.. In the time filter, click *Show dates*. -.. Click the start date, then *Absolute*. -.. Set the *Start date* to May 18, 2015. -.. Click *now*, then *Absolute*. -.. Set the *End date* to May 20, 2015. -.. Click *Update* +* To add your own data to {kib}, refer to <>. -. Map the geo coordinates from the log files. +* To search and filter your data, refer to {kibana-ref}/discover.html[Discover]. -.. Click *Add layer > Clusters and Grids*. -.. From the *Index pattern* dropdown, select *logstash*. -.. Click *Add layer*. +* To create a dashboard with your own data, refer to <>. -. Set the *Layer Style*. -.. From the *Fill color* dropdown, select the yellow to red color ramp. -.. From the *Border color* dropdown, select white. -.. Click *Save & close*. -+ -The map looks like this: -+ -[role="screenshot"] -image::images/tutorial-visualize-map-2.png[] +* To create maps that you can add to your dashboards, refer to <>. -. Navigate the map by clicking and dragging. Use the controls -to zoom the map and set filters. -. *Save* the map with the name `Map Example`. +* To create presentations of your live data, refer to <>. diff --git a/docs/glossary.asciidoc b/docs/glossary.asciidoc index 1edb33032418b..be24402170bbe 100644 --- a/docs/glossary.asciidoc +++ b/docs/glossary.asciidoc @@ -151,7 +151,7 @@ that you are interested in. A navigation path that retains context (time range and filters) from the source to the destination, so you can view the data from a new perspective. A dashboard that shows the overall status of multiple data centers -might have a drilldown to a dashboard for a single data center. See {kibana-ref}/drilldowns.html[Drilldowns]. +might have a drilldown to a dashboard for a single data center. See {kibana-ref}/dashboard.html[Drilldowns]. // end::drilldown-def[] @@ -238,7 +238,7 @@ support for scripted fields. See Enables you to build visualizations by dragging and dropping data fields. Lens makes makes smart visualization suggestions for your data, allowing you to switch between visualization types. -See {kibana-ref}/lens.html[Lens]. +See {kibana-ref}/dashboard.html[Lens]. // end::lens-def[] @@ -350,7 +350,7 @@ A {kib} control that constrains the search results to a particular time period. [[glossary-timelion]] Timelion :: // tag::timelion-def[] A tool for building a time series visualization that analyzes data in time order. -See {kibana-ref}/timelion.html[Timelion]. +See {kibana-ref}/dashboard.html[Timelion]. // end::timelion-def[] @@ -364,7 +364,7 @@ Timestamped data such as logs, metrics, and events that is indexed on an ongoing // tag::tsvb-def[] A time series data visualizer that allows you to combine an infinite number of aggregations to display complex data. -See {kibana-ref}/TSVB.html[TSVB]. +See {kibana-ref}/dashboard.html[TSVB]. // end::tsvb-def[] @@ -388,7 +388,7 @@ indices and guides you through resolving issues, including reindexing. See [[glossary-vega]] Vega :: // tag::vega-def[] A declarative language used to create interactive visualizations. -See {kibana-ref}/vega-graph.html[Vega]. +See {kibana-ref}/dashboard.html[Vega]. // end::vega-def[] [[glossary-vector]] vector data:: diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index 05036311c094c..7de2a042160e9 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -7,7 +7,7 @@ you want to work with. Once you create an index pattern, you're ready to: * Interactively explore your data in <>. -* Analyze your data in charts, tables, gauges, tag clouds, and more in <>. +* Analyze your data in charts, tables, gauges, tag clouds, and more in <>. * Show off your data in a <> workpad. * If your data includes geo data, visualize it with <>. diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index 51de5ad620b46..8c885ddca52e5 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -92,5 +92,5 @@ index pattern. This is useful if the index you were working with has been rename WARNING: Validation is not performed for object properties. Submitting an invalid change will render the object unusable. A more failsafe approach is to use -*Discover*, *Visualize*, or *Dashboard* to create new objects instead of +*Discover* or *Dashboard* to create new objects instead of directly editing an existing one. diff --git a/docs/management/numeral.asciidoc b/docs/management/numeral.asciidoc index 5d4d48ca785e1..a8834a3278a9e 100644 --- a/docs/management/numeral.asciidoc +++ b/docs/management/numeral.asciidoc @@ -10,7 +10,7 @@ Numeral formatting patterns are used in multiple places in {kib}, including: * <> * <> -* <> +* <> * <> The simplest pattern format is `0`, and the default {kib} pattern is `0,0.[000]`. diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 831b536f8c1cb..8aa57f50fe94b 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -60,7 +60,7 @@ You can read more at {ref}/rollup-job-config.html[rollup job configuration]. === Try it: Create and visualize rolled up data This example creates a rollup job to capture log data from sample web logs. -To follow along, add the <>. +To follow along, add the <>. In this example, you want data that is older than 7 days in the target index pattern `kibana_sample_data_logs` to roll up once a day into the index `rollup_logstash`. You’ll bucket the @@ -145,7 +145,7 @@ is `rollup_logstash,kibana_sample_data_logs`. In this index pattern, `rollup_log matches the rolled up index pattern and `kibana_sample_data_logs` matches the index pattern for raw data. -. Go to *Visualize* and create a vertical bar chart. +. Go to *Dashboard* and create a vertical bar chart. . Choose `rollup_logstash,kibana_sample_data_logs` as your source to see both the raw and rolled up data. diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 1a20c1df582e6..42d1d89145d79 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -59,7 +59,7 @@ This page has moved. Please see <>. [role="exclude",id="add-sample-data"] == Add sample data -This page has moved. Please see <>. +This page has moved. Please see <>. [role="exclude",id="tilemap"] == Coordinate map @@ -95,3 +95,8 @@ More information on this new feature is available in <>. == Role-based access control This content has moved to the <> page. + +[role="exclude",id="TSVB"] +== TSVB + +This page was deleted. See <>. diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index e02c7f212277e..88858c36643ec 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -37,12 +37,12 @@ You can configure the following settings in the `kibana.yml` file. [cols="2*<"] |=== -| `xpack.actions.whitelistedHosts` +| `xpack.actions.whitelistedHosts` {ess-icon} | A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly whitelisted. An empty list `[]` can be used to block built-in actions from making any external connections. + + Note that hosts associated with built-in actions, such as Slack and PagerDuty, are not automatically whitelisted. If you are not using the default `[*]` setting, you must ensure that the corresponding endpoints are whitelisted as well. -| `xpack.actions.enabledActionTypes` +| `xpack.actions.enabledActionTypes` {ess-icon} | A list of action types that are enabled. It defaults to `[*]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, and `.webhook`. An empty list `[]` will disable all action types. + + Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function. diff --git a/docs/settings/dev-settings.asciidoc b/docs/settings/dev-settings.asciidoc index e92e9c2928793..62553293a7d03 100644 --- a/docs/settings/dev-settings.asciidoc +++ b/docs/settings/dev-settings.asciidoc @@ -14,7 +14,7 @@ They are enabled by default. [cols="2*<"] |=== -| `xpack.grokdebugger.enabled` +| `xpack.grokdebugger.enabled` {ess-icon} | Set to `true` to enable the <>. Defaults to `true`. |=== diff --git a/docs/settings/graph-settings.asciidoc b/docs/settings/graph-settings.asciidoc index a66785242c19a..876e3dc936ccf 100644 --- a/docs/settings/graph-settings.asciidoc +++ b/docs/settings/graph-settings.asciidoc @@ -13,7 +13,7 @@ You do not need to configure any settings to use the {graph-features}. [cols="2*<"] |=== -| `xpack.graph.enabled` +| `xpack.graph.enabled` {ess-icon} | Set to `false` to disable the {graph-features}. |=== diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index d538519eefcc4..6c8632efa9cc0 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -37,6 +37,11 @@ For more information, see monitoring back-end does not run and {kib} stats are not sent to the monitoring cluster. +a|`monitoring.cluster_alerts.` +`email_notifications.email_address` {ess-icon} + | Specifies the email address where you want to receive cluster alerts. + See <> for details. + | `monitoring.ui.elasticsearch.hosts` | Specifies the location of the {es} cluster where your monitoring data is stored. By default, this is the same as `elasticsearch.hosts`. This setting enables @@ -85,7 +90,7 @@ These settings control how data is collected from {kib}. | Set to `true` (default) to enable data collection from the {kib} NodeJS server for {kib} dashboards to be featured in *{stack-monitor-app}*. -| `monitoring.kibana.collection.interval` +| `monitoring.kibana.collection.interval` {ess-icon} | Specifies the number of milliseconds to wait in between data sampling on the {kib} NodeJS server for the metrics that are displayed in the {kib} dashboards. Defaults to `10000` (10 seconds). @@ -111,7 +116,7 @@ about configuring {kib}, see | Set to `false` to hide *{stack-monitor-app}*. The monitoring back-end continues to run as an agent for sending {kib} stats to the monitoring cluster. Defaults to `true`. - + | `monitoring.ui.logs.index` | Specifies the name of the indices that are shown on the <> page in *{stack-monitor-app}*. The default value @@ -124,7 +129,7 @@ about configuring {kib}, see {ref}/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-size[Terms Aggregation]. Defaults to `10000`. -| `monitoring.ui.min_interval_seconds` +| `monitoring.ui.min_interval_seconds` {ess-icon} | Specifies the minimum number of seconds that a time bucket in a chart can represent. Defaults to 10. If you modify the `monitoring.ui.collection.interval` in `elasticsearch.yml`, use the same @@ -143,7 +148,7 @@ container, then Cgroup statistics are not useful. [cols="2*<"] |=== -| `monitoring.ui.container.elasticsearch.enabled` +| `monitoring.ui.container.elasticsearch.enabled` {ess-icon} | For {es} clusters that are running in containers, this setting changes the *Node Listing* to display the CPU utilization based on the reported Cgroup statistics. It also adds the calculated Cgroup CPU utilization to the diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 0b6f94e86a39f..9c8d753a2d668 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -17,10 +17,10 @@ You can configure `xpack.reporting` settings in your `kibana.yml` to: [cols="2*<"] |=== -| [[xpack-enable-reporting]]`xpack.reporting.enabled` +| [[xpack-enable-reporting]]`xpack.reporting.enabled` {ess-icon} | Set to `false` to disable the {report-features}. -| `xpack.reporting.encryptionKey` +| `xpack.reporting.encryptionKey` {ess-icon} | Set to an alphanumeric, at least 32 characters long text string. By default, {kib} will generate a random key when it starts, which will cause pending reports to fail after restart. Configure this setting to preserve the same key across multiple restarts and multiple instances of {kib}. @@ -86,7 +86,7 @@ reports, you might need to change the following settings. | How often the index that stores reporting jobs rolls over to a new index. Valid values are `year`, `month`, `week`, `day`, and `hour`. Defaults to `week`. -| `xpack.reporting.queue.pollEnabled` +| `xpack.reporting.queue.pollEnabled` {ess-icon} | Set to `true` (default) to enable the {kib} instance to to poll the index for pending jobs and claim them for execution. Setting this to `false` allows the {kib} instance to only add new jobs to the reporting queue, list jobs, and @@ -107,7 +107,7 @@ security is enabled, `xpack.security.encryptionKey`. | Specifies the number of milliseconds that the reporting poller waits between polling the index for any pending Reporting jobs. Defaults to `3000` (3 seconds). -| [[xpack-reporting-q-timeout]] `xpack.reporting.queue.timeout` +| [[xpack-reporting-q-timeout]] `xpack.reporting.queue.timeout` {ess-icon} | How long each worker has to produce a report. If your machine is slow or under heavy load, you might need to increase this timeout. Specified in milliseconds. If a Reporting job execution time goes over this time limit, the job will be @@ -125,19 +125,22 @@ control the capturing process. [cols="2*<"] |=== -| `xpack.reporting.capture.timeouts.openUrl` +a| `xpack.reporting.capture.timeouts` +`.openUrl` {ess-icon} | Specify how long to allow the Reporting browser to wait for the "Loading..." screen to dismiss and find the initial data for the Kibana page. If the time is exceeded, a page screenshot is captured showing the current state, and the download link shows a warning message. Defaults to `60000` (1 minute). -| `xpack.reporting.capture.timeouts.waitForElements` +a| `xpack.reporting.capture.timeouts` +`.waitForElements` {ess-icon} | Specify how long to allow the Reporting browser to wait for all visualization panels to load on the Kibana page. If the time is exceeded, a page screenshot is captured showing the current state, and the download link shows a warning message. Defaults to `30000` (30 seconds). -| `xpack.reporting.capture.timeouts.renderComplete` +a| `xpack.reporting.capture.timeouts` +`.renderComplete` {ess-icon} | Specify how long to allow the Reporting browser to wait for all visualizations to fetch and render the data. If the time is exceeded, a page screenshot is captured showing the current state, and the download link shows a warning message. Defaults to @@ -155,7 +158,7 @@ available, but there will likely be errors in the visualizations in the report. [cols="2*<"] |=== -| `xpack.reporting.capture.maxAttempts` +| `xpack.reporting.capture.maxAttempts` {ess-icon} | If capturing a report fails for any reason, {kib} will re-attempt other reporting job, as many times as this setting. Defaults to `3`. @@ -166,7 +169,7 @@ available, but there will likely be errors in the visualizations in the report. visualizations, try increasing this value. Defaults to `3000` (3 seconds). -| [[xpack-reporting-browser]] `xpack.reporting.capture.browser.type` +| [[xpack-reporting-browser]] `xpack.reporting.capture.browser.type` {ess-icon} | Specifies the browser to use to capture screenshots. This setting exists for backward compatibility. The only valid option is `chromium`. @@ -180,20 +183,24 @@ When `xpack.reporting.capture.browser.type` is set to `chromium` (default) you c [cols="2*<"] |=== -| `xpack.reporting.capture.browser.chromium.disableSandbox` +a| `xpack.reporting.capture.browser` +`.chromium.disableSandbox` | It is recommended that you research the feasibility of enabling unprivileged user namespaces. See Chromium Sandbox for additional information. Defaults to false for all operating systems except Debian, Red Hat Linux, and CentOS which use true. -| `xpack.reporting.capture.browser.chromium.proxy.enabled` +a| `xpack.reporting.capture.browser` +`.chromium.proxy.enabled` | Enables the proxy for Chromium to use. When set to `true`, you must also specify the `xpack.reporting.capture.browser.chromium.proxy.server` setting. Defaults to `false`. -| `xpack.reporting.capture.browser.chromium.proxy.server` +a| `xpack.reporting.capture.browser` +.chromium.proxy.server` | The uri for the proxy server. Providing the username and password for the proxy server via the uri is not supported. -| `xpack.reporting.capture.browser.chromium.proxy.bypass` +a| `xpack.reporting.capture.browser` +.chromium.proxy.bypass` | An array of hosts that should not go through the proxy server and should use a direct connection instead. Examples of valid entries are "elastic.co", "*.elastic.co", ".elastic.co", ".elastic.co:5601". @@ -205,27 +212,27 @@ When `xpack.reporting.capture.browser.type` is set to `chromium` (default) you c [cols="2*<"] |=== -| [[xpack-reporting-csv]] `xpack.reporting.csv.maxSizeBytes` +| [[xpack-reporting-csv]] `xpack.reporting.csv.maxSizeBytes` {ess-icon} | The maximum size of a CSV file before being truncated. This setting exists to prevent large exports from causing performance and storage issues. Defaults to `10485760` (10mB). | `xpack.reporting.csv.scroll.size` - | Number of documents retrieved from {es} for each scroll iteration during a CSV + | Number of documents retrieved from {es} for each scroll iteration during a CSV export. Defaults to `500`. | `xpack.reporting.csv.scroll.duration` | Amount of time allowed before {kib} cleans the scroll context during a CSV export. Defaults to `30s`. - + | `xpack.reporting.csv.checkForFormulas` | Enables a check that warns you when there's a potential formula involved in the output (=, -, +, and @ chars). See OWASP: https://www.owasp.org/index.php/CSV_Injection Defaults to `true`. - + | `xpack.reporting.csv.enablePanelActionDownload` - | Enables CSV export from a saved search on a dashboard. This action is available in the dashboard + | Enables CSV export from a saved search on a dashboard. This action is available in the dashboard panel menu for the saved search. Defaults to `true`. diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index a0995cab984d4..b6eecc6ea9f04 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -73,27 +73,27 @@ The valid settings in the `xpack.security.authc.providers` namespace vary depend [cols="2*<"] |=== | `xpack.security.authc.providers.` -`..enabled` +`..enabled` {ess-icon} | Determines if the authentication provider should be enabled. By default, {kib} enables the provider as soon as you configure any of its properties. | `xpack.security.authc.providers.` -`..order` +`..order` {ess-icon} | Order of the provider in the authentication chain and on the Login Selector UI. | `xpack.security.authc.providers.` -`..description` +`..description` {ess-icon} | Custom description of the provider entry displayed on the Login Selector UI. | `xpack.security.authc.providers.` -`..hint` +`..hint` {ess-icon} | Custom hint for the provider entry displayed on the Login Selector UI. | `xpack.security.authc.providers.` -`..icon` +`..icon` {ess-icon} | Custom icon for the provider entry displayed on the Login Selector UI. | `xpack.security.authc.providers.` -`..showInSelector` +`..showInSelector` {ess-icon} | Flag that indicates if the provider should have an entry on the Login Selector UI. Setting this to `false` doesn't remove the provider from the authentication chain. 2+a| @@ -104,7 +104,7 @@ You are unable to set this setting to `false` for `basic` and `token` authentica ============ | `xpack.security.authc.providers.` -`..accessAgreement.message` +`..accessAgreement.message` {ess-icon} | Access agreement text in Markdown format. For more information, refer to <>. |=== @@ -118,11 +118,11 @@ In addition to <.realm` +`saml..realm` {ess-icon} | SAML realm in {es} that provider should use. | `xpack.security.authc.providers.` -`saml..useRelayStateDeepLink` +`saml..useRelayStateDeepLink` {ess-icon} | Determines if the provider should treat the `RelayState` parameter as a deep link in {kib} during Identity Provider initiated log in. By default, this setting is set to `false`. The link specified in `RelayState` should be a relative, URL-encoded {kib} URL. For example, the `/app/dashboards#/list` link in `RelayState` parameter would look like this: `RelayState=%2Fapp%2Fdashboards%23%2Flist`. |=== @@ -136,7 +136,7 @@ In addition to <.realm` +`oidc..realm` {ess-icon} | OpenID Connect realm in {es} that the provider should use. |=== @@ -168,13 +168,13 @@ You can configure the following settings in the `kibana.yml` file. [cols="2*<"] |=== -| `xpack.security.loginAssistanceMessage` +| `xpack.security.loginAssistanceMessage` {ess-icon} | Adds a message to the login UI. Useful for displaying information about maintenance windows, links to corporate sign up pages, and so on. -| `xpack.security.loginHelp` +| `xpack.security.loginHelp` {ess-icon} | Adds a message accessible at the login UI with additional help information for the login process. -| `xpack.security.authc.selector.enabled` +| `xpack.security.authc.selector.enabled` {ess-icon} | Determines if the login selector UI should be enabled. By default, this setting is set to `true` if more than one authentication provider is configured. |=== @@ -203,12 +203,12 @@ You can configure the following settings in the `kibana.yml` file. this to `true` if SSL is configured outside of {kib} (for example, you are routing requests through a load balancer or proxy). -| `xpack.security.sameSiteCookies` +| `xpack.security.sameSiteCookies` {ess-icon} | Sets the `SameSite` attribute of the session cookie. This allows you to declare whether your cookie should be restricted to a first-party or same-site context. Valid values are `Strict`, `Lax`, `None`. This is *not set* by default, which modern browsers will treat as `Lax`. If you use Kibana embedded in an iframe in modern browsers, you might need to set it to `None`. Setting this value to `None` requires cookies to be sent over a secure connection by setting `xpack.security.secureCookies: true`. -| `xpack.security.session.idleTimeout` +| `xpack.security.session.idleTimeout` {ess-icon} | Ensures that user sessions will expire after a period of inactivity. This and `xpack.security.session.lifespan` are both highly recommended. By default, this setting is not set. @@ -218,7 +218,7 @@ highly recommended. By default, this setting is not set. The format is a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). ============ -| `xpack.security.session.lifespan` +| `xpack.security.session.lifespan` {ess-icon} | Ensures that user sessions will expire after the defined time period. This behavior also known as an "absolute timeout". If this is _not_ set, user sessions could stay active indefinitely. This and `xpack.security.session.idleTimeout` are both highly recommended. By default, this setting is not set. diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index f750784c47043..ea02afb8a9fda 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -11,7 +11,7 @@ To start working with your data in {kib}, you can: * Connect {kib} with existing {es} indices. -If you're not ready to use your own data, you can add a <> +If you're not ready to use your own data, you can add a <> to see all that you can do in {kib}. [float] diff --git a/docs/user/alerting/action-types.asciidoc b/docs/user/alerting/action-types.asciidoc index 1743edb10f92b..be31458ff39fa 100644 --- a/docs/user/alerting/action-types.asciidoc +++ b/docs/user/alerting/action-types.asciidoc @@ -25,7 +25,7 @@ a| <> a| <> -| Push or update data to a new incident in ServiceNow. +| Create an incident in ServiceNow. a| <> diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index 317ec67dd7c0a..0b0eb7a318495 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -137,7 +137,7 @@ image::images/canvas-map-embed.gif[] . To use the customization options, click the panel menu, then select one of the following options: -* *Edit map* — Opens <> or <> so that you can edit the original saved object. +* *Edit map* — Opens <> or a visualization builder so that you can edit the original saved object. * *Edit panel title* — Adds a title to the saved object. diff --git a/docs/user/dashboard.asciidoc b/docs/user/dashboard.asciidoc deleted file mode 100644 index b812af7e981bf..0000000000000 --- a/docs/user/dashboard.asciidoc +++ /dev/null @@ -1,191 +0,0 @@ -[[dashboard]] -= Dashboard - -[partintro] --- - -A _dashboard_ is a collection of visualizations, searches, and -maps, typically in real-time. Dashboards provide -at-a-glance insights into your data and enable you to drill down into details. - -With *Dashboard*, you can: - -* Add visualizations, saved searches, and maps for side-by-side analysis - -* Arrange dashboard elements to display exactly how you want - -* Customize time ranges to display only the data you want - -* Inspect and edit dashboard elements to find out exactly what kind of data is displayed - -[role="screenshot"] -image:images/Dashboard_example.png[Example dashboard] - -[float] -[[dashboard-read-only-access]] -=== [xpack]#Read only access# -If you see -the read-only icon in the application header, -then you don't have sufficient privileges to create and save dashboards. The buttons to create and edit -dashboards are not visible. For more information, see <>. - -[role="screenshot"] -image::images/dashboard-read-only-badge.png[Example of Dashboard read only access indicator in Kibana header] - --- - -[[dashboard-create-new-dashboard]] -== Create a dashboard - -To create a dashboard, you must have data indexed into {es}, an index pattern -to retrieve the data from {es}, and -visualizations, saved searches, or maps. If these don't exist, you're prompted to -add them as you create the dashboard, or you can add -<>, -which include pre-built dashboards. - -To begin, open the menu, go to *Dashboard*, then click *Create dashboard.* - -[float] -[[dashboard-add-elements]] -=== Add elements - -The visualizations, saved searches, and maps are stored as elements in panels -that you can move and resize. - -You can add elements from multiple indices, and the same element can appear in -multiple dashboards. - -To create an element: - -. Click *Create new*. -. On the *New Visualization* window, click the visualization type. -+ -[role="screenshot"] -image:images/Dashboard_add_new_visualization.png[Example add new visualization to dashboard] -+ -For information on how to create visualizations, see <>. -+ -For information on how to create maps, see <>. - -To add an existing element: - -. Click *Add*. - -. On the *Add panels* flyout, select the panel. -+ -When a dashboard element has a stored query, -both queries are applied. -+ -[role="screenshot"] -image:images/Dashboard_add_visualization.png[Example add visualization to dashboard] - -[float] -[[customizing-your-dashboard]] -=== Arrange dashboard elements - -In *Edit* mode, you can move, resize, customize, and delete panels to suit your needs. - -[[moving-containers]] -* To move a panel, click and hold the panel header and drag to the new location. - -[[resizing-containers]] -* To resize a panel, click the resize control and drag -to the new dimensions. - -* To toggle the use of margins and panel titles, use the *Options* menu. - -* To delete a panel, open the panel menu and select *Delete from dashboard.* Deleting a panel from a -dashboard does *not* delete the saved visualization or search. - -[float] -[[cloning-a-panel]] -=== Clone dashboard elements - -In *Edit* mode, you can clone any panel on a dashboard. - -To clone an existing panel, open the panel menu of the element you wish to clone, then select *Clone panel*. - -* Cloned panels appear beside the original, and will move other panels down to make room if necessary. - -* Clones support all of the original panel's functionality, including renaming, editing, and cloning. - -* All cloned visualizations will appear in the visualization list. - -[role="screenshot"] -image:images/clone_panel.gif[clone panel] - - -[float] -[[viewing-detailed-information]] -=== Inspect and edit elements - -Many dashboard elements allow you to drill down into the data and requests -behind the element. - -From the panel menu, select *Inspect*. -The data that displays depends on the element that you inspect. - -[role="screenshot"] -image:images/Dashboard_inspect.png[Inspect in dashboard] - -To open an element for editing, put the dashboard in *Edit* mode, -and then select *Edit visualization* from the panel menu. The changes you make appear in -every dashboard that uses the element. - -[float] -[[dashboard-customize-filter]] -=== Customize time ranges - -You can configure each visualization, saved search, and map on your dashboard -for a specific time range. For example, you might want one visualization to show -the monthly trend for CPU usage and another to show the current CPU usage. - -From the panel menu, select *Customize time range* to expose a time filter -dedicated to that panel. Panels that are not restricted by a specific -time range are controlled by the -global time filter. - -[role="screenshot"] -image:images/time_range_per_panel.gif[Time range per dashboard panel] - -[float] -[[save-dashboards]] -=== Save the dashboard - -When you're finished adding and arranging the panels, save the dashboard. - -. In the {kib} toolbar, click *Save*. - -. Enter the dashboard *Title* and optional *Description*, then *Save* the dashboard. - -include::{kib-repo-dir}/drilldowns/drilldowns.asciidoc[] -include::{kib-repo-dir}/drilldowns/explore-underlying-data.asciidoc[] - -[[sharing-dashboards]] -== Share the dashboard - -[[embedding-dashboards]] -Share your dashboard outside of {kib}. - -From the *Share* menu, you can: - -* Embed the code in a web page. Users must have {kib} access -to view an embedded dashboard. -* Share a direct link to a {kib} dashboard -* Generate a PDF report -* Generate a PNG report - -TIP: To create a link to a dashboard by title, use: + -`${domain}/${basepath?}/app/dashboards#/list?title=${yourdashboardtitle}` - -TIP: When sharing a link to a dashboard snapshot, use the *Short URL*. Snapshot -URLs are long and can be problematic for Internet Explorer and other -tools. To create a short URL, you must have write access to {kib}. - -[float] -[[import-dashboards]] -=== Export the dashboard - -To export the dashboard, open the menu, then click *Stack Management > Saved Objects*. For more information, -refer to <>. diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc new file mode 100644 index 0000000000000..1bcea3bb36aea --- /dev/null +++ b/docs/user/dashboard/aggregation-reference.asciidoc @@ -0,0 +1,242 @@ +[[aggregation-reference]] +== Aggregation reference + +{kib} supports many types of {ref}/search-aggregations.html[{es} aggregations] that you can use to build complex summaries of your data. + +By using a series of {es} aggregations to extract and process your data, you can create panels that tell a +story about the trends, patterns, and outliers in your data. + +[float] +[[bucket-aggregations]] +=== Bucket aggregations + +For information about Elasticsearch bucket aggregations, refer to {ref}/search-aggregations-bucket.html[Bucket aggregations]. + +[options="header"] +|=== + +| Type | Visualizations | Data table | Markdown | Lens | TSVB + +| Histogram +^| X +^| X +^| X +| +| + +| Date histogram +^| X +^| X +^| X +^| X +^| X + +| Date range +^| X +^| X +^| X +| +| + +| Filter +^| X +^| X +^| X +| +^| X + +| Filters +^| X +^| X +^| X +| +^| X + +| GeoHash grid +^| X +^| X +^| X +| +| + +| IP range +^| X +^| X +^| X +| +| + +| Range +^| X +^| X +^| X +| +| + +| Terms +^| X +^| X +^| X +^| X +^| X + +| Significant terms +^| X +^| X +^| X +| +^| X + +|=== + +[float] +[[metrics-aggregations]] +=== Metrics aggregations + +For information about Elasticsearch metrics aggregations, refer to {ref}/search-aggregations-metrics.html[Metrics aggregations]. + +[options="header"] +|=== + +| Type | Visualizations | Data table | Markdown | Lens | TSVB + +| Average +^| X +^| X +^| X +^| X +^| X + +| Sum +^| X +^| X +^| X +^| X +^| X + +| Unique count (Cardinality) +^| X +^| X +^| X +^| X +^| X + +| Max +^| X +^| X +^| X +^| X +^| X + +| Min +^| X +^| X +^| X +^| X +^| X + +| Percentiles +^| X +^| X +^| X +| +^| X + +| Percentiles Rank +^| X +^| X +^| X +| +^| X + +| Top hit +^| X +^| X +^| X +| +^| X + +| Value count +| +| +| +| +^| X + +|=== + +[float] +[[pipeline-aggregations]] +=== Pipeline aggregations + +For information about Elasticsearch pipeline aggregations, refer to {ref}/search-aggregations-pipeline.html[Pipeline aggregations]. + +[options="header"] +|=== + +| Type | Visualizations | Data table | Markdown | Lens | TSVB + +| Avg bucket +^| X +^| X +^| X +| +^| X + +| Derivative +^| X +^| X +^| X +| +^| X + +| Max bucket +^| X +^| X +^| X +| +^| X + +| Min bucket +^| X +^| X +^| X +| +^| X + +| Sum bucket +^| X +^| X +^| X +^| +^| X + +| Moving average +^| X +^| X +^| X +^| +^| X + +| Cumulative sum +^| X +^| X +^| X +^| +^| X + +| Bucket script +| +| +| +| +^| X + +| Serial differencing +^| X +^| X +^| X +| +^| X + +|=== diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc new file mode 100644 index 0000000000000..0c0151cc3ace2 --- /dev/null +++ b/docs/user/dashboard/dashboard.asciidoc @@ -0,0 +1,472 @@ +[[dashboard]] += Dashboard + +[partintro] +-- + +A _dashboard_ is a collection of panels that you use to analyze your data. On a dashboard, you can add a variety of panels that +you can rearrange and tell a story about your data. Panels contain everything you need, including visualizations, +interactive controls, markdown, and more. + +With *Dashboard*s, you can: + +* Add multiple panels to see many aspects and views of your data in one place. + +* Arrange panels for analysis and comparison. + +* Add text and images to provide context to the panels and make them easy to consume. + +* Create and apply filters to focus on the data you want to display. + +* Control who can use your data, and share the dashboard with a small or large audience. + +* Generate reports based on your findings. + +To begin, open the menu, go to *Dashboard*, then click *Create dashboard*. + +[role="screenshot"] +image:images/Dashboard_example.png[Example dashboard] + +[float] +[[dashboard-read-only-access]] +=== [xpack]#Read only access# +If you see +the read-only icon in the application header, +then you don't have sufficient privileges to create and save dashboards. The buttons to create and edit +dashboards are not visible. For more information, see <>. + +[role="screenshot"] +image::images/dashboard-read-only-badge.png[Example of Dashboard read only access indicator in Kibana header] + +[float] +[[types-of-panels]] +== Types of panels + +Panels contain everything you need to tell a story about you data, including visualizations, +interactive controls, Markdown, and more. + +[cols="50, 50"] +|=== + +a| *Area* + +Displays data points, connected by a line, where the area between the line and axes are shaded. +Use area charts to compare two or more categories over time, and display the magnitude of trends. + +| image:images/area.png[Area chart] + +a| *Stacked area* + +Displays the evolution of the value of several data groups. The values of each group are displayed +on top of each other. Use stacked area charts to visualize part-to-whole relationships, and to show +how each category contributes to the cumulative total. + +| image:images/stacked_area.png[Stacked area chart] + +a| *Bar* + +Displays bars side-by-side where each bar represents a category. Use bar charts to compare data across a +large number of categories, display data that includes categories with negative values, and easily identify +the categories that represent the highest and lowest values. Kibana also supports horizontal bar charts. + +| image:images/bar.png[Bar chart] + +a| *Stacked bar* + +Displays numeric values across two or more categories. Use stacked bar charts to compare numeric values between +levels of a categorical value. Kibana also supports stacked horizontal bar charts. + +| image:images/stacked_bar.png[Stacked area chart] + + +a| *Line* + +Displays data points that are connected by a line. Use line charts to visualize a sequence of values, discover +trends over time, and forecast future values. + +| image:images/line.png[Line chart] + +a| *Pie* + +Displays slices that represent a data category, where the slice size is proportional to the quantity it represents. +Use pie charts to show comparisons between multiple categories, illustrate the dominance of one category over others, +and show percentage or proportional data. + +| image:images/pie.png[Pie chart] + +a| *Donut* + +Similar to the pie chart, but the central circle is removed. Use donut charts when you’d like to display multiple statistics at once. + +| image:images/donut.png[Donut chart] + + +a| *Tree map* + +Relates different segments of your data to the whole. Each rectangle is subdivided into smaller rectangles, or sub branches, based on +its proportion to the whole. Use treemaps to make efficient use of space to show percent total for each category. + +| image:images/treemap.png[Tree map] + + +a| *Heat map* + +Displays graphical representations of data where the individual values are represented by colors. Use heat maps when your data set includes +categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data. + +| image:images/heat_map.png[Heat map] + +a| *Goal* + +Displays how your metric progresses toward a fixed goal. Use the goal to display an easy to read visual of the status of your goal progression. + +| image:images/goal.png[Goal] + + +a| *Gauge* + +Displays your data along a scale that changes color according to where your data falls on the expected scale. Use the gauge to show how metric +values relate to reference threshold values, or determine how a specified field is performing versus how it is expected to perform. + +| image:images/gauge.png[Gauge] + + +a| *Metric* + +Displays a single numeric value for an aggregation. Use the metric visualization when you have a numeric value that is powerful enough to tell +a story about your data. + +| image:images/metric.png[Metric] + + +a| *Data table* + +Displays your raw data or aggregation results in a tabular format. Use data tables to display server configuration details, track counts, min, +or max values for a specific field, and monitor the status of key services. + +| image:images/data_table.png[Data table] + + +a| *Tag cloud* + +Graphical representations of how frequently a word appears in the source text. Use tag clouds to easily produce a summary of large documents and +create visual art for a specific topic. + +| image:images/tag_cloud.png[Tag cloud] + + +a| *Maps* + +For all your mapping needs, use <>. + +| image:images/maps.png[Maps] + + +|=== + +[float] +[[create-panels]] +== Create panels + +To create a panel, make sure you have {ref}/getting-started-index.html[data indexed into {es}] and an <> +to retrieve the data from {es}. If you aren’t ready to use your own data, {kib} comes with several pre-built dashboards that you can test out. For more information, +refer to <>. + +To begin, click *Create new*, then choose one of the following options on the +*New Visualization* window: + +* Click on the type of panel you want to create, then configure the options. + +* Select an editor to help you create the panel. + +[role="screenshot"] +image:images/Dashboard_add_new_visualization.png[Example add new visualization to dashboard] + +{kib} provides you with several editors that help you create panels. + +[float] +[[lens]] +=== Create panels with Lens + +*Lens* is the simplest and fastest way to create powerful visualizations of your data. To use *Lens*, you drag and drop as many data fields +as you want onto the visualization builder pane, and *Lens* uses heuristics to decide how to apply each field to the visualization. + +With *Lens*, you can: + +* Use the automatically generated suggestions to change the visualization type. +* Create visualizations with multiple layers and indices. +* Change the aggregation and labels to customize the data. + +[role="screenshot"] +image::images/lens_drag_drop.gif[Drag and drop] + +TIP: Drag-and-drop capabilities are available only when *Lens* knows how to use the data. If *Lens* is unable to automatically generate a +visualization, configure the customization options for your visualization. + +[float] +[[fiter-the-data-fields]] +==== Filter the data fields + +The data fields that are displayed are based on the selected <> and the <>. + +To view the data fields in a different index pattern, click the index pattern, then select a new one. The data fields automatically update. + +To filter the data fields: + +* Enter the name in the *Search field names*. +* Click *Field by type*, then select the filter. To show all fields in the index pattern, deselect *Only show fields with data*. + +[float] +[[view-data-summaries]] +==== View data summaries + +To help you decide exactly the data you want to display, get a quick summary of each field. The summary shows the distribution of +values within the specified time range. + +To view the data field summary information, navigate to the field, then click *i*. + +[role="screenshot"] +image::images/lens_data_info.png[Data summary window] + +[float] +[[change-the-visualization-type]] +==== Change the visualization type + +Use the automatically generated suggestions to change the visualization type, or manually select the type of visualization you want to view. + +*Suggestions* are shortcuts to alternative visualizations that *Lens* generates for you. + +[role="screenshot"] +image::images/lens_suggestions.gif[Visualization suggestions] + +If you’d like to use a visualization type outside of the suggestions, click the visualization type, then select a new one. + +[role="screenshot"] +image::images/lens_viz_types.png[] + +When there is an exclamation point (!) next to a visualization type, *Lens* is unable to transfer your data, but still allows you to make the change. + +[float] +[[customize-the-data]] +==== Customize the data + +For each visualization type, you can customize the aggregation and labels. The options available depend on the selected visualization type. + +. Click a data field name in the editor, or click *Drop a field here*. +. Change the options that appear. ++ +[role="screenshot"] +image::images/lens_aggregation_labels.png[Quick function options] + +[float] +[[add-layers-and-indices]] +==== Add layers and indices + +To compare and analyze data from different sources, you can visualize multiple data layers and indices. Multiple layers and indices are +supported in area, line, and bar charts. + +To add a layer, click *+*, then drag and drop the data fields for the new layer. + +[role="screenshot"] +image::images/lens_layers.png[Add layers] + +To view a different index, click the index name in the editor, then select a new one. + +[role="screenshot"] +image::images/lens_index_pattern.png[Add index pattern] + +Ready to try out *Lens*? Refer to the <>. + +[float] +[[tsvb]] +=== Create panels with TSVB + +*TSVB* is a time series data visualizer that allows you to use the full power of the Elasticsearch aggregation framework. To use *TSVB*, +you can combine an infinite number of <> to display your data. + +With *TSVB*, you can: + +* Create visualizations, data tables, and markdown panels. +* Create visualizations with multiple indices. +* Change the aggregation and labels to customize the data. ++ +[role="screenshot"] +image::images/tsvb.png[TSVB UI] + +[float] +[[configure-the-data]] +==== Configure the data + +With *TSVB*, you can add and display multiple data sets to compare and analyze. {kib} uses many types of <> that you can use to build +complex summaries of that data. + +. Select *Data*. If you are using *Table*, select *Columns*. +. From the *Aggregation* drop down, select the aggregation you want to visualize. ++ +If you don’t see any data, change the <>. ++ +To add multiple aggregations, click *+*. +. From the *Group by* drop down, select how you want to group or split the data. +. To add another data set, click *+*. ++ +When you have more than one aggregation, the last value is displayed, which is indicated by the eye icon. + +[float] +[[change-the-data-display]] +==== Change the data display + +To find the best way to display your data, *TSVB* supports several types of panels and charts. + +To change the *Time Series* chart type: + +. Click *Data > Options*. +. Select the *Chart type*. + +To change the panel type, click on the panel options: + +[role="screenshot"] +image::images/tsvb_change_display.gif[TSVB change the panel type] + +[float] +[[custommize-the-data]] +==== Customize the data + +View data in a different <>, and change the data label name and colors. The options available depend on the panel type. + +To change the index pattern, click *Panel options*, then enter the new *Index Pattern*. + +To override the index pattern for a data set, click *Data > Options*. Select *Yes* to override, then enter the new *Index pattern*. + +To change the data labels and colors: + +. Click *Data*. +. Enter the *Label* name, which *TSVB* uses on the legends and data labels. +. Click the color picker, then select the color for the data. ++ +[role="screenshot"] +image::images/tsvb_color_picker.png[TSVB color picker] + +[float] +[[add-annotations]] +==== Add annotations + +You can overlay annotation events on top of your *Time Series* charts. The options available depend on the data source. + +To begin, click *Annotations*, click *Add data source*, then configure the options. + +[role="screenshot"] +image::images/tsvb_annotations.png[TSVB annotations] + +[float] +[[filter-the-panel]] +==== Filter the panel + +The data that displays on the panel is based on the <> and <>. +You can filter the data on the panels using the <>. + +Click *Panel options*, then enter the syntax in the *Panel Filter* field. + +If you want to ignore filters from all of {kib}, select *Yes* for *Ignore global filter*. + +[float] +[[vega]] +=== Create custom panels with Vega + +Build custom visualizations using *Vega* and *Vega-Lite*, backed by one or more data sources including {es}, Elastic Map Service, +URL, or static data. Use the {kib} extensions to embed *Vega* in your dashboard, and add interactive tools. + +Use *Vega* and *Vega-Lite* when you want to create a visualization for: + +* Aggregations that use `nested` or `parent/child` mapping +* Aggregations without an index pattern +* Queries that use custom time filters +* Complex calculations +* Extracting data from _source instead of aggregations +* Scatter charts, sankey charts, and custom maps +* Using an unsupported visual theme + +[role="screenshot"] +image::images/vega.png[Vega UI] + +*Vega* and *Vega-Lite* are declarative formats that: + +* Create complex visualizations +* Use JSON and a different syntax for declaring visualizations +* Are not fully interchangeable + +For more information about *Vega* and *Vega-Lite*, refer to: + +* <> +* <> +* <> +* <> + +[float] +[[timelion]] +=== Create panels with Timelion + +*Timelion* is a time series data visualizer that enables you to combine independent data sources within a single visualization. + +*Timelion* is driven by a simple expression language that you use to: + +* Retrieve time series data +* Perform calculation to tease out the answers to complex questions +* Visualize the results + +[role="screenshot"] +image::images/timelion.png[Timelion UI] + +Ready to try out Timelion? For step-by-step tutorials, refer to: + +* <> +* <> +* <> + +[float] +[[save-panels]] +=== Save panels + +When you’ve finished making changes, save the panels. + +. Click *Save*. +. Add the *Title* and optional *Description*. +. Click *Save and return*. + +[float] +[[add-existing-panels]] +== Add existing panels + +Add panels that you’ve already created to your dashboard. + +On the dashboard, click *Add an existing*, then select the panel you want to add. + +When a panel contains a stored query, both queries are applied. + +[role="screenshot"] +image:images/Dashboard_add_visualization.png[Example add visualization to dashboard] + +To make changes to the panel, put the dashboard in *Edit* mode, then select the edit option from the panel menu. +The changes you make appear in every dashboard that uses the panel, except if you edit the panel title. Changes to the panel title appear only on the dashboard where you made the change. + +[float] +[[save-dashboards]] +== Save dashboards + +When you’ve finished adding the panels, save the dashboard. + +. In the toolbar, click *Save*. + +. Enter the dashboard *Title* and optional *Description*, then *Save* the dashboard. + +-- +include::edit-dashboards.asciidoc[] + +include::explore-dashboard-data.asciidoc[] + +include::share-dashboards.asciidoc[] + +include::tutorials.asciidoc[] + +include::aggregation-reference.asciidoc[] + +include::vega-reference.asciidoc[] diff --git a/docs/drilldowns/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc similarity index 93% rename from docs/drilldowns/drilldowns.asciidoc rename to docs/user/dashboard/drilldowns.asciidoc index e2dfaa5af39ce..5fca974d58135 100644 --- a/docs/drilldowns/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -1,5 +1,6 @@ +[float] [[drilldowns]] -== Use drilldowns for dashboard actions +=== Use drilldowns for dashboard actions Drilldowns, also known as custom actions, allow you to configure a workflow for analyzing and troubleshooting your data. @@ -13,7 +14,7 @@ that shows a single data center or server. [float] [[how-drilldowns-work]] -=== How drilldowns work +==== How drilldowns work Drilldowns are user-configurable {kib} actions that are stored with the dashboard metadata. Drilldowns are specific to the dashboard panel @@ -35,7 +36,7 @@ to learn how to code drilldowns. [float] [[create-manage-drilldowns]] -=== Create and manage drilldowns +==== Create and manage drilldowns Your dashboard must be in *Edit* mode to create a drilldown. Once a panel has at least one drilldown, the menu also includes a *Manage drilldowns* action @@ -46,14 +47,13 @@ image::images/drilldown_menu.png[Panel menu with Create drilldown and Manage dri [float] [[drilldowns-example]] -=== Try it: Create a drilldown +==== Try it: Create a drilldown This example shows how to create the *Host Overview* drilldown shown earlier in this doc. -[float] -==== Set up the dashboards +*Set up the dashboards* -. Add the <> data set. +. Add the <> data set. . Create a new dashboard, called `Host Overview`, and include these visualizations from the sample data set: @@ -74,9 +74,7 @@ TIP: If you don’t see data for a panel, try changing the time range. Search: `extension.keyword:( “gz” or “css” or “deb”)` Filter: `geo.src : CN` -[float] -==== Create the drilldown - +*Create the drilldown* . In the dashboard menu bar, click *Edit*. diff --git a/docs/user/dashboard/edit-dashboards.asciidoc b/docs/user/dashboard/edit-dashboards.asciidoc new file mode 100644 index 0000000000000..7534ea1e9e9fb --- /dev/null +++ b/docs/user/dashboard/edit-dashboards.asciidoc @@ -0,0 +1,115 @@ +[[edit-dashboards]] +== Edit dashboards + +Now that you have added panels to your dashboard, you can add filter panels to interact with the data, and Markdown panels to add context to the dashboard. +To make your dashboard look the way you want, use the editing options. + +[float] +[[add-controls]] +=== Add controls + +To filter the data on your dashboard in real-time, add a *Controls* panel. + +You can add two types of *Controls*: + +* Options list — Filters content based on one or more specified options. The dropdown menu is dynamically populated with the results of a terms aggregation. +For example, use the options list on the sample flight dashboard when you want to filter the data by origin city and destination city. + +* Range slider — Filters data within a specified range of numbers. The minimum and maximum values are dynamically populated with the results of a +min and max aggregation. For example, use the range slider when you want to filter the sample flight dashboard by a specific average ticket price. + +[role="screenshot"] +image::images/dashboard-controls.png[] + +To configure *Controls* for your dashboard: + +. Click *Options*, then configure the following: + +* *Update Kibana filters on each change* — When selected, all interactive inputs create filters that refresh the dashboard. When unselected, + {kib} filters are created only when you click *Apply changes*. + +* *Use time filter* — When selected, the aggregations that generate the options list and time range are connected to the <>. + +* *Pin filters to global state* — When selected, all filters created by interacting with the inputs are automatically pinned. + +. Click *Update*. + +[float] +[[add-markdown]] +=== Add Markdown + +*Markdown* is a text entry field that accepts GitHub-flavored Markdown text. When you enter the text, the tool populates the results on the dashboard. + +Use Markdown when you want to add context to the other panels on your dashboard, such as important information, instructions and images. + +For information about GitHub-flavored Markdown text, click *Help*. + +For example, when you enter: + +[role="screenshot"] +image::images/markdown_example_1.png[] + +The following instructions are displayed: + +[role="screenshot"] +image::images/markdown_example_2.png[] + +Or when you enter: + +[role="screenshot"] +image::images/markdown_example_3.png[] + +The following image is displayed: + +[role="screenshot"] +image::images/markdown_example_4.png[] + +[float] +[[arrange-panels]] +[[moving-containers]] +[[resizing-containers]] +=== Arrange panels + +To make your dashboard panels look exactly how you want, you can move, resize, customize, and delete them. + +Put the dashboard in *Edit* mode, then use the following options: + +* To move, click and hold the panel header, then drag to the new location. + +* To resize, click the resize control, then drag to the new dimensions. + +* To delete, open the panel menu, then select Delete from dashboard. When you delete a panel from the dashboard, the +visualization or saved search from the panel is still available in Kibana. + +[float] +[[clone-panels]] +=== Clone panels + +To duplicate a panel and its configured functionality, clone the panel. Cloned panels support all of the original functionality, +including renaming, editing, and cloning. + +. Put the dashboard in *Edit* mode. + +. For the panel you want to clone, open the panel menu, then select *Clone panel*. + +Cloned panels appear beside the original, and move other panels down to make room when necessary. +All cloned visualization panels appear in the visualization list. + +[role="screenshot"] +image:images/clone_panel.gif[clone panel] + +[float] +[[dashboard-customize-filter]] +=== Customize time ranges + +You can configure each visualization, saved search, and map on your dashboard +for a specific time range. For example, you might want one visualization to show +the monthly trend for CPU usage and another to show the current CPU usage. + +From the panel menu, select *Customize time range* to expose a time filter +dedicated to that panel. Panels that are not restricted by a specific +time range are controlled by the +<>. + +[role="screenshot"] +image:images/time_range_per_panel.gif[Time range per dashboard panel] diff --git a/docs/user/dashboard/explore-dashboard-data.asciidoc b/docs/user/dashboard/explore-dashboard-data.asciidoc new file mode 100644 index 0000000000000..a0564f5bceb3d --- /dev/null +++ b/docs/user/dashboard/explore-dashboard-data.asciidoc @@ -0,0 +1,20 @@ +[[explore-dashboard-data]] +== Explore dashboard data + +Get a closer look at your data by inspecting elements and using drilldown actions. + +[float] +[[viewing-detailed-information]] +=== Inspect elements + +To view the data and requests behind the visualizations and saved searches, you can drill down into the elements. + +From the panel menu, select *Inspect*. +The data that displays depends on the element that you inspect. + +[role="screenshot"] +image:images/Dashboard_inspect.png[Inspect in dashboard] + +include::explore-underlying-data.asciidoc[] + +include::drilldowns.asciidoc[] diff --git a/docs/user/dashboard/explore-underlying-data.asciidoc b/docs/user/dashboard/explore-underlying-data.asciidoc new file mode 100644 index 0000000000000..9b7be21dc45d2 --- /dev/null +++ b/docs/user/dashboard/explore-underlying-data.asciidoc @@ -0,0 +1,27 @@ +[float] +[[explore-the-underlying-data]] +=== Explore the underlying data for panels + +To explore the underlying data of the panels on your dashboard, {kib} opens *Discover*, +where you can view and filter the data in the visualization panel. When {kib} opens *Discover*, the index pattern, filters, query, and time range for the visualization continue to apply. + +TIP: The *Explore underlying data* option is available only for visualization panels with a single index pattern. + +To use the *Explore underlying data* option: + +* Click the from the panel menu, then click *Explore underlying data*. ++ +[role="screenshot"] +image::images/explore_data_context_menu.png[Explore underlying data from panel context menu] + +* Interact with the chart, then click *Explore underlying data* on the menu that appears. ++ +[role="screenshot"] +image::images/explore_data_in_chart.png[Explore underlying data from chart] ++ +To enable, open `kibana.yml`, then add the following: + +["source","yml"] +----------- +xpack.discoverEnhanced.actions.exploreDataInChart.enabled: true +----------- diff --git a/docs/user/dashboard/images/area.png b/docs/user/dashboard/images/area.png new file mode 100644 index 0000000000000..85d21a9e178c5 Binary files /dev/null and b/docs/user/dashboard/images/area.png differ diff --git a/docs/user/dashboard/images/bar.png b/docs/user/dashboard/images/bar.png new file mode 100644 index 0000000000000..f1db847655947 Binary files /dev/null and b/docs/user/dashboard/images/bar.png differ diff --git a/docs/user/dashboard/images/data_table.png b/docs/user/dashboard/images/data_table.png new file mode 100644 index 0000000000000..3e08ec526ba57 Binary files /dev/null and b/docs/user/dashboard/images/data_table.png differ diff --git a/docs/user/dashboard/images/donut.png b/docs/user/dashboard/images/donut.png new file mode 100644 index 0000000000000..a662f58ba553b Binary files /dev/null and b/docs/user/dashboard/images/donut.png differ diff --git a/docs/drilldowns/images/drilldown_create.png b/docs/user/dashboard/images/drilldown_create.png similarity index 100% rename from docs/drilldowns/images/drilldown_create.png rename to docs/user/dashboard/images/drilldown_create.png diff --git a/docs/drilldowns/images/drilldown_menu.png b/docs/user/dashboard/images/drilldown_menu.png similarity index 100% rename from docs/drilldowns/images/drilldown_menu.png rename to docs/user/dashboard/images/drilldown_menu.png diff --git a/docs/drilldowns/images/drilldown_on_panel.png b/docs/user/dashboard/images/drilldown_on_panel.png similarity index 100% rename from docs/drilldowns/images/drilldown_on_panel.png rename to docs/user/dashboard/images/drilldown_on_panel.png diff --git a/docs/drilldowns/images/drilldown_on_piechart.gif b/docs/user/dashboard/images/drilldown_on_piechart.gif similarity index 100% rename from docs/drilldowns/images/drilldown_on_piechart.gif rename to docs/user/dashboard/images/drilldown_on_piechart.gif diff --git a/docs/drilldowns/images/explore_data_context_menu.png b/docs/user/dashboard/images/explore_data_context_menu.png similarity index 100% rename from docs/drilldowns/images/explore_data_context_menu.png rename to docs/user/dashboard/images/explore_data_context_menu.png diff --git a/docs/drilldowns/images/explore_data_in_chart.png b/docs/user/dashboard/images/explore_data_in_chart.png similarity index 100% rename from docs/drilldowns/images/explore_data_in_chart.png rename to docs/user/dashboard/images/explore_data_in_chart.png diff --git a/docs/user/dashboard/images/gauge.png b/docs/user/dashboard/images/gauge.png new file mode 100644 index 0000000000000..c4aef7f5f6854 Binary files /dev/null and b/docs/user/dashboard/images/gauge.png differ diff --git a/docs/user/dashboard/images/goal.png b/docs/user/dashboard/images/goal.png new file mode 100644 index 0000000000000..967e64f722d74 Binary files /dev/null and b/docs/user/dashboard/images/goal.png differ diff --git a/docs/user/dashboard/images/heat_map.png b/docs/user/dashboard/images/heat_map.png new file mode 100644 index 0000000000000..d4a6502509f6f Binary files /dev/null and b/docs/user/dashboard/images/heat_map.png differ diff --git a/docs/user/dashboard/images/lens_aggregation_labels.png b/docs/user/dashboard/images/lens_aggregation_labels.png new file mode 100644 index 0000000000000..9dcf1d226a197 Binary files /dev/null and b/docs/user/dashboard/images/lens_aggregation_labels.png differ diff --git a/docs/user/dashboard/images/lens_data_info.png b/docs/user/dashboard/images/lens_data_info.png new file mode 100644 index 0000000000000..5ea6fc64a217d Binary files /dev/null and b/docs/user/dashboard/images/lens_data_info.png differ diff --git a/docs/user/dashboard/images/lens_drag_drop.gif b/docs/user/dashboard/images/lens_drag_drop.gif new file mode 100644 index 0000000000000..ca62115e7ea3a Binary files /dev/null and b/docs/user/dashboard/images/lens_drag_drop.gif differ diff --git a/docs/user/dashboard/images/lens_index_pattern.png b/docs/user/dashboard/images/lens_index_pattern.png new file mode 100644 index 0000000000000..90a34b7a5d225 Binary files /dev/null and b/docs/user/dashboard/images/lens_index_pattern.png differ diff --git a/docs/user/dashboard/images/lens_layers.png b/docs/user/dashboard/images/lens_layers.png new file mode 100644 index 0000000000000..7410425a6977e Binary files /dev/null and b/docs/user/dashboard/images/lens_layers.png differ diff --git a/docs/user/dashboard/images/lens_suggestions.gif b/docs/user/dashboard/images/lens_suggestions.gif new file mode 100644 index 0000000000000..3258e924cb205 Binary files /dev/null and b/docs/user/dashboard/images/lens_suggestions.gif differ diff --git a/docs/user/dashboard/images/lens_viz_types.png b/docs/user/dashboard/images/lens_viz_types.png new file mode 100644 index 0000000000000..2ecfa6bd0e0e3 Binary files /dev/null and b/docs/user/dashboard/images/lens_viz_types.png differ diff --git a/docs/user/dashboard/images/line.png b/docs/user/dashboard/images/line.png new file mode 100644 index 0000000000000..123fa74dc7e14 Binary files /dev/null and b/docs/user/dashboard/images/line.png differ diff --git a/docs/user/dashboard/images/maps.png b/docs/user/dashboard/images/maps.png new file mode 100644 index 0000000000000..65336451cc1c7 Binary files /dev/null and b/docs/user/dashboard/images/maps.png differ diff --git a/docs/user/dashboard/images/metric.png b/docs/user/dashboard/images/metric.png new file mode 100644 index 0000000000000..f8182d538a608 Binary files /dev/null and b/docs/user/dashboard/images/metric.png differ diff --git a/docs/user/dashboard/images/pie.png b/docs/user/dashboard/images/pie.png new file mode 100644 index 0000000000000..927fbb98adc07 Binary files /dev/null and b/docs/user/dashboard/images/pie.png differ diff --git a/docs/user/dashboard/images/stacked_area.png b/docs/user/dashboard/images/stacked_area.png new file mode 100644 index 0000000000000..ae66fc51176f9 Binary files /dev/null and b/docs/user/dashboard/images/stacked_area.png differ diff --git a/docs/user/dashboard/images/stacked_bar.png b/docs/user/dashboard/images/stacked_bar.png new file mode 100644 index 0000000000000..aa90ce3685cff Binary files /dev/null and b/docs/user/dashboard/images/stacked_bar.png differ diff --git a/docs/user/dashboard/images/tag_cloud.png b/docs/user/dashboard/images/tag_cloud.png new file mode 100644 index 0000000000000..976c456e4a1f1 Binary files /dev/null and b/docs/user/dashboard/images/tag_cloud.png differ diff --git a/docs/user/dashboard/images/timelion.png b/docs/user/dashboard/images/timelion.png new file mode 100644 index 0000000000000..a663791575077 Binary files /dev/null and b/docs/user/dashboard/images/timelion.png differ diff --git a/docs/user/dashboard/images/treemap.png b/docs/user/dashboard/images/treemap.png new file mode 100644 index 0000000000000..5df3c9526bfeb Binary files /dev/null and b/docs/user/dashboard/images/treemap.png differ diff --git a/docs/user/dashboard/images/tsvb.png b/docs/user/dashboard/images/tsvb.png new file mode 100644 index 0000000000000..09a3c7e86eb56 Binary files /dev/null and b/docs/user/dashboard/images/tsvb.png differ diff --git a/docs/user/dashboard/images/tsvb_annotations.png b/docs/user/dashboard/images/tsvb_annotations.png new file mode 100644 index 0000000000000..510f3c2672118 Binary files /dev/null and b/docs/user/dashboard/images/tsvb_annotations.png differ diff --git a/docs/user/dashboard/images/tsvb_change_display.gif b/docs/user/dashboard/images/tsvb_change_display.gif new file mode 100644 index 0000000000000..09d435b0a6b24 Binary files /dev/null and b/docs/user/dashboard/images/tsvb_change_display.gif differ diff --git a/docs/user/dashboard/images/tsvb_color_picker.png b/docs/user/dashboard/images/tsvb_color_picker.png new file mode 100644 index 0000000000000..4f033579d0005 Binary files /dev/null and b/docs/user/dashboard/images/tsvb_color_picker.png differ diff --git a/docs/user/dashboard/images/vega.png b/docs/user/dashboard/images/vega.png new file mode 100644 index 0000000000000..6a0d8cb772adf Binary files /dev/null and b/docs/user/dashboard/images/vega.png differ diff --git a/docs/user/dashboard/share-dashboards.asciidoc b/docs/user/dashboard/share-dashboards.asciidoc new file mode 100644 index 0000000000000..cfa146d60fdac --- /dev/null +++ b/docs/user/dashboard/share-dashboards.asciidoc @@ -0,0 +1,27 @@ +[[share-dashboards]] +== Share dashboards + +[[embedding-dashboards]] +Share your dashboard outside of {kib}. + +From the *Share* menu, you can: + +* Embed the code in a web page. Users must have {kib} access +to view an embedded dashboard. +* Share a direct link to a {kib} dashboard +* Generate a PDF report +* Generate a PNG report + +TIP: To create a link to a dashboard by title, use: + +`${domain}/${basepath?}/app/dashboards#/list?title=${yourdashboardtitle}` + +TIP: When sharing a link to a dashboard snapshot, use the *Short URL*. Snapshot +URLs are long and can be problematic for Internet Explorer and other +tools. To create a short URL, you must have write access to {kib}. + +[float] +[[import-dashboards]] +=== Export the dashboard + +To export the dashboard, open the menu, then click *Stack Management > Saved Objects*. For more information, +refer to <>. \ No newline at end of file diff --git a/docs/visualize/vega.asciidoc b/docs/user/dashboard/tutorials.asciidoc similarity index 60% rename from docs/visualize/vega.asciidoc rename to docs/user/dashboard/tutorials.asciidoc index b231159e86bde..931720ccbe257 100644 --- a/docs/visualize/vega.asciidoc +++ b/docs/user/dashboard/tutorials.asciidoc @@ -1,36 +1,79 @@ -[[vega-graph]] -== Vega +[[tutorials]] +== Tutorials -Build custom visualizations using Vega and Vega-Lite, backed by one or more -data sources including {es}, Elastic Map Service, URL, -or static data. Use the {kib} extensions to Vega to embed Vega into -your dashboard, and to add interactivity to the visualizations. +Learn how to use *Lens*, *Vega*, and *Timelion* by going through one of the step-by-step tutorials. -Vega and Vega-Lite are both declarative formats to create visualizations -using JSON. Both use a different syntax for declaring visualizations, -and are not fully interchangeable. +[[lens-tutorial]] +=== Compare sales over time with Lens + +Ready to create your own visualization with Lens? Use the following tutorial to create a visualization that lets you compare sales over time. + +[float] +[[lens-before-begin]] +==== Before you begin + +To start, you'll need to add the <>. + +[float] +==== Build the visualization + +Drag and drop your data onto the visualization builder pane. + +. Select the *kibana_sample_data_ecommerce* index pattern. + +. Click image:images/time-filter-calendar.png[], then click *Last 7 days*. ++ +The fields in the data panel update. + +. Drag and drop the *taxful_total_price* data field to the visualization builder pane. ++ +[role="screenshot"] +image::images/lens_tutorial_1.png[Lens tutorial] + +To display the average order prices over time, *Lens* automatically added in *order_date* field. + +To break down your data, drag the *category.keyword* field to the visualization builder pane. Lens +knows that you want to show the top categories and compare them across the dates, +and creates a chart that compares the sales for each of the top three categories: + +[role="screenshot"] +image::images/lens_tutorial_2.png[Lens tutorial] [float] -[[when-to-vega]] -=== When to use Vega - -Vega and Vega-Lite are capable of building most of the visualizations -that {kib} provides, but with higher complexity. The most common reason -to use Vega in {kib} is that {kib} is missing support for the query or -visualization, for example: - -* Aggregations using the `nested` or `parent/child` mapping -* Aggregations without a {kib} index pattern -* Queries using custom time filters -* Complex calculations -* Extracting data from _source instead of aggregation -* Scatter charts -* Sankey charts -* Custom maps -* Using a visual theme that {kib} does not provide - -[[vega-lite-tutorial]] -=== Tutorial: First visualization in Vega-Lite +[[customize-lens-visualization]] +==== Customize your visualization + +Make your visualization look exactly how you want with the customization options. + +. Click *Average of taxful_total_price*, then change the *Label* to `Sales`. ++ +[role="screenshot"] +image::images/lens_tutorial_3.1.png[Lens tutorial] + +. Click *Top values of category.keyword*, then change *Number of values* to `10`. ++ +[role="screenshot"] +image::images/lens_tutorial_3.2.png[Lens tutorial] ++ +The visualization updates to show there are only six available categories. ++ +Look at the *Suggestions*. An area chart is not an option, but for the sales data, a stacked area chart might be the best option. + +. To switch the chart type, click *Stacked bar chart* in the column, then click *Stacked area* from the *Select a visualizations* window. ++ +[role="screenshot"] +image::images/lens_tutorial_3.png[Lens tutorial] + +[float] +[[lens-tutorial-next-steps]] +==== Next steps + +Now that you've created your visualization, you can add it to a <> or <>. + +[[vega-lite-tutorial-create-your-first-visualizations]] +=== Create your first visualization with Vega-Lite + +experimental[] In this tutorial, you will learn about how to edit Vega-Lite in {kib} to create a stacked area chart from an {es} search query. It will give you a starting point @@ -65,6 +108,7 @@ which is similar to JSON but optimized for human editing. HJSON supports: * Multiline strings [float] +[[small-steps]] ==== Small steps Always work on Vega in the smallest steps possible, and save your work frequently. @@ -633,8 +677,10 @@ The final result of this tutorial is this spec: ==== -[[vega-tutorial]] -=== Tutorial: Updating {kib} filters from Vega +[[vega-tutorial-update-kibana-filters-from-vega]] +=== Update {kib} filters from Vega + +experimental[] In this tutorial you will build an area chart in Vega using an {es} search query, and add a click handler and drag handler to update {kib} filters. @@ -1225,415 +1271,486 @@ The final Vega spec for this tutorial is here: ---- ==== -[[vega-reference]] -=== Reference for {kib} extensions - -{kib} has extended Vega and Vega-Lite with extensions that support: - -* Default height and width -* Default theme to match {kib} -* Writing {es} queries using the time range and filters from dashboards -* Using the Elastic Map Service in Vega maps -* Additional tooltip styling -* Advanced setting to enable URL loading from any domain -* Limited debugging support using the browser dev tools -* (Vega only) Expression functions which can update the time range and dashboard filters - -[[vega-sizing-and-positioning]] -==== Default height and width +[[timelion-tutorial-create-time-series-visualizations]] +=== Create time series visualizations with Timelion -By default, Vega visualizations use the `autosize = { type: 'fit', contains: 'padding' }` layout. -`fit` uses all available space, ignores `width` and `height` values, -and respects the padding values. To override this behavior, change the -`autosize` value. +To compare the real-time percentage of CPU time spent in user space to the results offset by one hour, create a time series visualization. -[[vega-theme]] -==== Default theme to match {kib} - -{kib} registers a default https://vega.github.io/vega/docs/schemes/[Vega color scheme] -with the id `elastic`, and sets a default color for each `mark` type. -Override it by providing a different `stroke`, `fill`, or `color` (Vega-Lite) value. - -[[vega-queries]] -==== Writing {es} queries in Vega - -{kib} extends the Vega https://vega.github.io/vega/docs/data/[data] elements -with support for direct {es} queries specified as a `url`. - -Because of this, {kib} is **unable to support dynamically loaded data**, -which would otherwise work in Vega. All data is fetched before it's passed to -the Vega renderer. +[float] +[[define-the-functions]] +==== Define the functions -To define an {es} query in Vega, set the `url` to an object. {kib} will parse -the object looking for special tokens that allow your query to integrate with {kib}. -These tokens are: +To start tracking the real-time percentage of CPU, enter the following in the *Timelion Expression* field: -* `%context%: true`: Set at the top level, and replaces the `query` section with filters from dashboard -* `%timefield%: `: Set at the top level, integrates the query with the dashboard time filter -* `{%timefilter%: true}`: Replaced by an {es} range query with upper and lower bounds -* `{%timefilter%: "min" | "max"}`: Replaced only by the upper or lower bounds -* `{%timefilter: true, shift: -1, unit: 'hour'}`: Generates a time range query one hour in the past -* `{%autointerval%: true}`: Replaced by the string which contains the automatic {kib} time interval, such as `1h` -* `{%autointerval%: 10}`: Replaced by a string which is approximately dividing the time into 10 ranges, allowing - you to influence the automatic interval -* `"%dashboard_context-must_clause%"`: String replaced by object containing filters -* `"%dashboard_context-filter_clause%"`: String replaced by an object containing filters -* `"%dashboard_context-must_not_clause%"`: String replaced by an object containing filters +[source,text] +---------------------------------- +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') +---------------------------------- -Putting this together, an example query that counts the number of documents in -a specific index: +[role="screenshot"] +image::images/timelion-create01.png[] +{nbsp} -[source,yaml] ----- -// An object instead of a string for the URL value -// is treated as a context-aware Elasticsearch query. -url: { - // Specify the time filter. - %timefield%: @timestamp - // Apply dashboard context filters when set - %context%: true - - // Which indexes to search - index: kibana_sample_data_logs - // The body element may contain "aggs" and "query" keys - body: { - aggs: { - time_buckets: { - date_histogram: { - // Use date histogram aggregation on @timestamp field - field: @timestamp <1> - // interval value will depend on the time filter - // Use an integer to set approximate bucket count - interval: { %autointerval%: true } - // Make sure we get an entire range, even if it has no data - extended_bounds: { - min: { %timefilter%: "min" } - max: { %timefilter%: "max" } - } - // Use this for linear (e.g. line, area) graphs - // Without it, empty buckets will not show up - min_doc_count: 0 - } - } - } - // Speed up the response by only including aggregation results - size: 0 - } -} ----- +[float] +[[compare-the-data]] +==== Compare the data -<1> `@timestamp` — Filters the time range and breaks it into histogram -buckets. +To compare the two data sets, add another series with data from the previous hour, separated by a comma: -The full result includes the following structure: +[source,text] +---------------------------------- +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct'), +.es(offset=-1h, <1> + index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') +---------------------------------- -[source,yaml] ----- -{ - "aggregations": { - "time_buckets": { - "buckets": [{ - "key_as_string": "2015-11-30T22:00:00.000Z", - "key": 1448920800000,<1> - "doc_count": 28 - }, { - "key_as_string": "2015-11-30T23:00:00.000Z", - "key": 1448924400000, <1> - "doc_count": 330 - }, ... ----- +<1> `offset` offsets the data retrieval by a date expression. In this example, `-1h` offsets the data back by one hour. -<1> `"key"` — The unix timestamp you can use without conversions by the -Vega date expressions. +[role="screenshot"] +image::images/timelion-create02.png[] +{nbsp} -For most visualizations, you only need the list of bucket values. To focus on -only the data you need, use `format: {property: "aggregations.time_buckets.buckets"}`. +[float] +[[add-label-names]] +==== Add label names -Specify a query with individual range and dashboard context. The query is -equivalent to `"%context%": true, "%timefield%": "@timestamp"`, -except that the time range is shifted back by 10 minutes: +To easily distinguish between the two data sets, add the label names: -[source,yaml] ----- -{ - body: { - query: { - bool: { - must: [ - // This string will be replaced - // with the auto-generated "MUST" clause - "%dashboard_context-must_clause%" - { - range: { - // apply timefilter (upper right corner) - // to the @timestamp variable - @timestamp: { - // "%timefilter%" will be replaced with - // the current values of the time filter - // (from the upper right corner) - "%timefilter%": true - // Only work with %timefilter% - // Shift current timefilter by 10 units back - shift: 10 - // week, day (default), hour, minute, second - unit: minute - } - } - } - ] - must_not: [ - // This string will be replaced with - // the auto-generated "MUST-NOT" clause - "%dashboard_context-must_not_clause%" - ] - filter: [ - // This string will be replaced - // with the auto-generated "FILTER" clause - "%dashboard_context-filter_clause%" - ] - } - } - } -} ----- +[source,text] +---------------------------------- +.es(offset=-1h,index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct').label('last hour'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct').label('current hour') <1> +---------------------------------- -NOTE: When using `"%context%": true` or defining a value for `"%timefield%"` the body cannot contain a query. To customize the query within the VEGA specification (e.g. add an additional filter, or shift the timefilter), define your query and use the placeholders as in the example above. The placeholders will be replaced by the actual context of the dashboard or visualization once parsed. +<1> `.label()` adds custom labels to the visualization. -The `"%timefilter%"` can also be used to specify a single min or max -value. The date_histogram's `extended_bounds` can be set -with two values - min and max. Instead of hardcoding a value, you may -use `"min": {"%timefilter%": "min"}`, which will be replaced with the -beginning of the current time range. The `shift` and `unit` values are -also supported. The `"interval"` can also be set dynamically, depending -on the currently picked range: `"interval": {"%autointerval%": 10}` will -try to get about 10-15 data points (buckets). +[role="screenshot"] +image::images/timelion-create03.png[] +{nbsp} [float] -[[vega-esmfiles]] -=== Access Elastic Map Service files - -Access the Elastic Map Service files via the same mechanism: +[[add-a-title]] +==== Add a title + +Add a meaningful title: + +[source,text] +---------------------------------- +.es(offset=-1h, + index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('last hour'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('current hour') + .title('CPU usage over time') <1> +---------------------------------- + +<1> `.title()` adds a title with a meaningful name. Titles make is easier for unfamiliar users to understand the purpose of the visualization. -[source,yaml] ----- -url: { - // "type" defaults to "elasticsearch" otherwise - type: emsfile - // Name of the file, exactly as in the Region map visualization - name: World Countries -} -// The result is a geojson file, get its features to use -// this data source with the "shape" marks -// https://vega.github.io/vega/docs/marks/shape/ -format: {property: "features"} ----- - -To enable Maps, the graph must specify `type=map` in the host -configuration: - -[source,yaml] ----- -{ - "config": { - "kibana": { - "type": "map", +[role="screenshot"] +image::images/timelion-customize01.png[] +{nbsp} - // Initial map position - "latitude": 40.7, // default 0 - "longitude": -74, // default 0 - "zoom": 7, // default 2 +[float] +[[change-the-chart-type]] +==== Change the chart type + +To differentiate between the current hour data and the last hour data, change the chart type: + +[source,text] +---------------------------------- +.es(offset=-1h, + index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('last hour') + .lines(fill=1,width=0.5), <1> +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('current hour') + .title('CPU usage over time') +---------------------------------- + +<1> `.lines()` changes the appearance of the chart lines. In this example, `.lines(fill=1,width=0.5)` sets the fill level to `1`, and the border width to `0.5`. - // defaults to "default". Use false to disable base layer. - "mapStyle": false, +[role="screenshot"] +image::images/timelion-customize02.png[] +{nbsp} - // default 0 - "minZoom": 5, +[float] +[[change-the-line-colors]] +==== Change the line colors + +To make the current hour data stand out, change the line colors: + +[source,text] +---------------------------------- +.es(offset=-1h, + index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('last hour') + .lines(fill=1,width=0.5) + .color(gray), <1> +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('current hour') + .title('CPU usage over time') + .color(#1E90FF) +---------------------------------- + +<1> `.color()` changes the color of the data. Supported color types include standard color names, hexadecimal values, or a color schema for grouped data. In this example, `.color(gray)` represents the last hour, and `.color(#1E90FF)` represents the current hour. - // defaults to the maximum for the given style, - // or 25 when base is disabled - "maxZoom": 13, +[role="screenshot"] +image::images/timelion-customize03.png[] +{nbsp} - // defaults to true, shows +/- buttons to zoom in/out - "zoomControl": false, +[float] +[[make-adjustments-to-the-legend]] +==== Make adjustments to the legend + +Change the position and style of the legend: + +[source,text] +---------------------------------- +.es(offset=-1h, + index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('last hour') + .lines(fill=1,width=0.5) + .color(gray), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('current hour') + .title('CPU usage over time') + .color(#1E90FF) + .legend(columns=2, position=nw) <1> +---------------------------------- + +<1> `.legend()` sets the position and style of the legend. In this example, `.legend(columns=2, position=nw)` places the legend in the north west position of the visualization with two columns. - // Defaults to 'false', disables mouse wheel zoom. If set to - // 'true', map may zoom unexpectedly while scrolling dashboard - "scrollWheelZoom": false, +[role="screenshot"] +image::images/timelion-customize04.png[] +{nbsp} - // When false, repaints on each move frame. - // Makes the graph slower when moving the map - "delayRepaint": true, // default true - } - }, - /* the rest of Vega JSON */ -} ----- +[[timelion-tutorial-create-visualizations-with-mathematical-functions]] +=== Timelion tutorial: Create visualizations with mathematical functions -The visualization automatically injects a `"projection"`, which you can use to -calculate the position of all geo-aware marks. -Additionally, you can use `latitude`, `longitude`, and `zoom` signals. -These signals can be used in the graph, or can be updated to modify the -position of the map. +To create a visualization for inbound and outbound network traffic, use mathematical functions. [float] -[[vega-tooltip]] -==== Additional tooltip styling +[[mathematical-functions-define-functions]] +==== Define the functions -{kib} has installed the https://vega.github.io/vega-lite/docs/tooltip.html[Vega tooltip plugin], -so tooltips can be defined in the ways documented there. Beyond that, {kib} also supports -a configuration option for changing the tooltip position and padding: +To start tracking the inbound and outbound network traffic, enter the following in the *Timelion Expression* field: -```js -{ - config: { - kibana: { - tooltips: { - position: 'top', - padding: 15 - } - } - } -} -``` +[source,text] +---------------------------------- +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) +---------------------------------- -[[vega-url-loading]] -==== Advanced setting to enable URL loading from any domain +[role="screenshot"] +image::images/timelion-math01.png[] +{nbsp} -Vega can load data from any URL, but this is disabled by default in {kib}. -To change this, set `vis_type_vega.enableExternalUrls: true` in `kibana.yml`, -then restart {kib}. +[float] +[[mathematical-functions-plot-change]] +==== Plot the rate of change -[[vega-inspector]] -==== Vega Inspector -Use the contextual *Inspect* tool to gain insights into different elements. -For Vega visualizations, there are two different views: *Request* and *Vega debug*. +Change how the data is displayed so that you can easily monitor the inbound traffic: -===== Inspect Elasticsearch requests +[source,text] +---------------------------------- +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) + .derivative() <1> +---------------------------------- -Vega uses the {ref}/search-search.html[{es} search API] to get documents and aggregation -results from {es}. To troubleshoot these requests, click *Inspect*, which shows the most recent requests. -In case your specification has more than one request, you can switch between the views using the *View* dropdown. +<1> `.derivative` plots the change in values over time. [role="screenshot"] -image::visualize/images/vega_tutorial_inspect_requests.png[] - -===== Vega debugging - -With the *Vega debug* view, you can inspect the *Data sets* and *Signal Values* runtime data. - -The runtime data is read from the -https://vega.github.io/vega/docs/api/debugging/#scope[runtime scope]. +image::images/timelion-math02.png[] +{nbsp} + +Add a similar calculation for outbound traffic: + +[source,text] +---------------------------------- +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) + .derivative(), +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.out.bytes) + .derivative() + .multiply(-1) <1> +---------------------------------- + +<1> `.multiply()` multiplies the data series by a number, the result of a data series, or a list of data series. For this example, `.multiply(-1)` converts the outbound network traffic to a negative value since the outbound network traffic is leaving your machine. [role="screenshot"] -image::visualize/images/vega_tutorial_inspect_data_sets.png[] - -To debug more complex specs, access to the `view` variable. For more information, refer to -the <>. +image::images/timelion-math03.png[] +{nbsp} -===== Asking for help with a Vega spec - -Because of the dynamic nature of the data in {es}, it is hard to help you with -Vega specs unless you can share a dataset. To do this, click *Inspect*, select the *Vega debug* view, -then select the *Spec* tab: +[float] +[[mathematical-functions-convert-data]] +==== Change the data metric + +To make the visualization easier to analyze, change the data metric from bytes to megabytes: + +[source,text] +---------------------------------- +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) + .derivative() + .divide(1048576), +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.out.bytes) + .derivative() + .multiply(-1) + .divide(1048576) <1> +---------------------------------- + +<1> `.divide()` accepts the same input as `.multiply()`, then divides the data series by the defined divisor. [role="screenshot"] -image::visualize/images/vega_tutorial_getting_help.png[] +image::images/timelion-math04.png[] +{nbsp} -To copy the response, click *Copy to clipboard*. Paste the copied data to -https://gist.github.com/[gist.github.com], possibly with a .json extension. Use the [raw] button, -and share that when asking for help. +[float] +[[mathematical-functions-add-labels]] +==== Customize and format the visualization + +Customize and format the visualization using functions: + +[source,text] +---------------------------------- +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) + .derivative() + .divide(1048576) + .lines(fill=2, width=1) + .color(green) + .label("Inbound traffic") <1> + .title("Network traffic (MB/s)"), <2> +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.out.bytes) + .derivative() + .multiply(-1) + .divide(1048576) + .lines(fill=2, width=1) <3> + .color(blue) <4> + .label("Outbound traffic") + .legend(columns=2, position=nw) <5> +---------------------------------- + +<1> `.label()` adds custom labels to the visualization. +<2> `.title()` adds a title with a meaningful name. +<3> `.lines()` changes the appearance of the chart lines. In this example, `.lines(fill=2, width=1)` sets the fill level to `2`, and the border width to `1`. +<4> `.color()` changes the color of the data. Supported color types include standard color names, hexadecimal values, or a color schema for grouped data. In this example, `.color(green)` represents the inbound network traffic, and `.color(blue)` represents the outbound network traffic. +<5> `.legend()` sets the position and style of the legend. For this example, `legend(columns=2, position=nw)` places the legend in the north west position of the visualization with two columns. -[[vega-browser-debugging-console]] -==== Browser debugging console +[role="screenshot"] +image::images/timelion-math05.png[] +{nbsp} -experimental[] Use browser debugging tools (for example, F12 or Ctrl+Shift+J in Chrome) to -inspect the `VEGA_DEBUG` variable: +[[timelion-tutorial-create-visualizations-withconditional-logic-and-tracking-trends]] +=== Create visualizations with conditional logic and tracking trends using Timelion -* `view` — Access to the Vega View object. See https://vega.github.io/vega/docs/api/debugging/[Vega Debugging Guide] -on how to inspect data and signals at runtime. For Vega-Lite, -`VEGA_DEBUG.view.data('source_0')` gets the pre-transformed data, and `VEGA_DEBUG.view.data('data_0')` -gets the encoded data. For Vega, it uses the data name as defined in your Vega spec. +To easily detect outliers and discover patterns over time, modify time series data with conditional logic and create a trend with a moving average. -* `vega_spec` — Vega JSON graph specification after some modifications by {kib}. In case -of Vega-Lite, this is the output of the Vega-Lite compiler. +With Timelion conditional logic, you can use the following operator values to compare your data: -* `vegalite_spec` — If this is a Vega-Lite graph, JSON specification of the graph before -Vega-Lite compilation. +[horizontal] +`eq`:: equal +`ne`:: not equal +`lt`:: less than +`lte`:: less than or equal to +`gt`:: greater than +`gte`:: greater than or equal to [float] -[[vega-expression-functions]] -==== (Vega only) Expression functions which can update the time range and dashboard filters +[[conditional-define-functions]] +==== Define the functions -{kib} has extended the Vega expression language with these functions: - -```js -/** - * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor - * @param {string} [index] as defined in Kibana, or default if missing - */ -kibanaAddFilter(query, index) - -/** - * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor - * @param {string} [index] as defined in Kibana, or default if missing - */ -kibanaRemoveFilter(query, index) - -kibanaRemoveAllFilters() - -/** - * Update dashboard time filter to the new values - * @param {number|string|Date} start - * @param {number|string|Date} end - */ -kibanaSetTimeFilter(start, end) -``` +To chart the maximum value of `system.memory.actual.used.bytes`, enter the following in the *Timelion Expression* field: -[float] -[[vega-additional-configuration-options]] -==== Additional configuration options +[source,text] +---------------------------------- +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') +---------------------------------- -[source,yaml] ----- -{ - config: { - kibana: { - // Placement of the Vega-defined signal bindings. - // Can be `left`, `right`, `top`, or `bottom` (default). - controlsLocation: top - // Can be `vertical` or `horizontal` (default). - controlsDirection: vertical - // If true, hides most of Vega and Vega-Lite warnings - hideWarnings: true - // Vega renderer to use: `svg` or `canvas` (default) - renderer: canvas - } - } -} ----- +[role="screenshot"] +image::images/timelion-conditional01.png[] +{nbsp} +[float] +[[conditional-track-memory]] +==== Track used memory + +To track the amount of memory used, create two thresholds: + +[source,text] +---------------------------------- +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt, <1> + 11300000000, <2> + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null) + .label('warning') + .color('#FFCC11'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt, + 11375000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null) + .label('severe') + .color('red') +---------------------------------- + +<1> Timelion conditional logic for the _greater than_ operator. In this example, the warning threshold is 11.3GB (`11300000000`), and the severe threshold is 11.375GB (`11375000000`). If the threshold values are too high or low for your machine, adjust the values accordingly. +<2> `if()` compares each point to a number. If the condition evaluates to `true`, adjust the styling. If the condition evaluates to `false`, use the default styling. -[[vega-notes]] -[[vega-useful-links]] -=== Resources and examples +[role="screenshot"] +image::images/timelion-conditional02.png[] +{nbsp} -To learn more about Vega and Vega-Lite, refer to the resources and examples. +[float] +[[conditional-determine-trend]] +==== Determine the trend + +To determine the trend, create a new data series: + +[source,text] +---------------------------------- +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt,11300000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null) + .label('warning') + .color('#FFCC11'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt,11375000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null). + label('severe') + .color('red'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .mvavg(10) <1> +---------------------------------- + +<1> `mvavg()` calculates the moving average over a specified period of time. In this example, `.mvavg(10)` creates a moving average with a window of 10 data points. -==== Vega editor -The https://vega.github.io/editor/[Vega Editor] includes examples for Vega & Vega-Lite, but does not support any -{kib}-specific features like {es} requests and interactive base maps. +[role="screenshot"] +image::images/timelion-conditional03.png[] +{nbsp} -==== Vega-Lite resources -* https://vega.github.io/vega-lite/tutorials/getting_started.html[Tutorials] -* https://vega.github.io/vega-lite/docs/[Docs] -* https://vega.github.io/vega-lite/examples/[Examples] +[float] +[[conditional-format-visualization]] +==== Customize and format the visualization + +Customize and format the visualization using functions: + +[source,text] +---------------------------------- +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .label('max memory') <1> + .title('Memory consumption over time'), <2> +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt, + 11300000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null) + .label('warning') + .color('#FFCC11') <3> + .lines(width=5), <4> +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt, + 11375000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null) + .label('severe') + .color('red') + .lines(width=5), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .mvavg(10) + .label('mvavg') + .lines(width=2) + .color(#5E5E5E) + .legend(columns=4, position=nw) <5> +---------------------------------- + +<1> `.label()` adds custom labels to the visualization. +<2> `.title()` adds a title with a meaningful name. +<3> `.color()` changes the color of the data. Supported color types include standard color names, hexadecimal values, or a color schema for grouped data. +<4> `.lines()` changes the appearance of the chart lines. In this example, .lines(width=5) sets border width to `5`. +<5> `.legend()` sets the position and style of the legend. For this example, `(columns=4, position=nw)` places the legend in the north west position of the visualization with four columns. -==== Vega resources -* https://vega.github.io/vega/tutorials/[Tutorials] -* https://vega.github.io/vega/docs/[Docs] -* https://vega.github.io/vega/examples/[Examples] +[role="screenshot"] +image::images/timelion-conditional04.png[] +{nbsp} -TIP: When you use the examples in {kib}, you may -need to modify the "data" section to use absolute URL. For example, -replace `"url": "data/world-110m.json"` with -`"url": "https://vega.github.io/editor/data/world-110m.json"`. +For additional information on Timelion conditional capabilities, go to https://www.elastic.co/blog/timeseries-if-then-else-with-timelion[I have but one .condition()]. \ No newline at end of file diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc new file mode 100644 index 0000000000000..eed8d9a35b874 --- /dev/null +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -0,0 +1,437 @@ +[[vega-reference]] +== Vega reference + +experimental[] + +For additional *Vega* and *Vega-Lite* information, refer to the reference sections. + +[float] +[[reference-for-kibana-extensions]] +=== Reference for {kib} extensions + +{kib} has extended Vega and Vega-Lite with extensions that support: + +* Default height and width +* Default theme to match {kib} +* Writing {es} queries using the time range and filters from dashboards +* Using the Elastic Map Service in Vega maps +* Additional tooltip styling +* Advanced setting to enable URL loading from any domain +* Limited debugging support using the browser dev tools +* (Vega only) Expression functions which can update the time range and dashboard filters + +[float] +[[vega-sizing-and-positioning]] +==== Default height and width + +By default, Vega visualizations use the `autosize = { type: 'fit', contains: 'padding' }` layout. +`fit` uses all available space, ignores `width` and `height` values, +and respects the padding values. To override this behavior, change the +`autosize` value. + +[float] +[[vega-theme]] +==== Default theme to match {kib} + +{kib} registers a default https://vega.github.io/vega/docs/schemes/[Vega color scheme] +with the id `elastic`, and sets a default color for each `mark` type. +Override it by providing a different `stroke`, `fill`, or `color` (Vega-Lite) value. + +[float] +[[vega-queries]] +==== Writing {es} queries in Vega + +experimental[] {kib} extends the Vega https://vega.github.io/vega/docs/data/[data] elements +with support for direct {es} queries specified as a `url`. + +Because of this, {kib} is **unable to support dynamically loaded data**, +which would otherwise work in Vega. All data is fetched before it's passed to +the Vega renderer. + +To define an {es} query in Vega, set the `url` to an object. {kib} will parse +the object looking for special tokens that allow your query to integrate with {kib}. +These tokens are: + +* `%context%: true`: Set at the top level, and replaces the `query` section with filters from dashboard +* `%timefield%: `: Set at the top level, integrates the query with the dashboard time filter +* `{%timefilter%: true}`: Replaced by an {es} range query with upper and lower bounds +* `{%timefilter%: "min" | "max"}`: Replaced only by the upper or lower bounds +* `{%timefilter: true, shift: -1, unit: 'hour'}`: Generates a time range query one hour in the past +* `{%autointerval%: true}`: Replaced by the string which contains the automatic {kib} time interval, such as `1h` +* `{%autointerval%: 10}`: Replaced by a string which is approximately dividing the time into 10 ranges, allowing + you to influence the automatic interval +* `"%dashboard_context-must_clause%"`: String replaced by object containing filters +* `"%dashboard_context-filter_clause%"`: String replaced by an object containing filters +* `"%dashboard_context-must_not_clause%"`: String replaced by an object containing filters + +Putting this together, an example query that counts the number of documents in +a specific index: + +[source,yaml] +---- +// An object instead of a string for the URL value +// is treated as a context-aware Elasticsearch query. +url: { + // Specify the time filter. + %timefield%: @timestamp + // Apply dashboard context filters when set + %context%: true + + // Which indexes to search + index: kibana_sample_data_logs + // The body element may contain "aggs" and "query" keys + body: { + aggs: { + time_buckets: { + date_histogram: { + // Use date histogram aggregation on @timestamp field + field: @timestamp <1> + // interval value will depend on the time filter + // Use an integer to set approximate bucket count + interval: { %autointerval%: true } + // Make sure we get an entire range, even if it has no data + extended_bounds: { + min: { %timefilter%: "min" } + max: { %timefilter%: "max" } + } + // Use this for linear (e.g. line, area) graphs + // Without it, empty buckets will not show up + min_doc_count: 0 + } + } + } + // Speed up the response by only including aggregation results + size: 0 + } +} +---- + +<1> `@timestamp` — Filters the time range and breaks it into histogram +buckets. + +The full result includes the following structure: + +[source,yaml] +---- +{ + "aggregations": { + "time_buckets": { + "buckets": [{ + "key_as_string": "2015-11-30T22:00:00.000Z", + "key": 1448920800000,<1> + "doc_count": 28 + }, { + "key_as_string": "2015-11-30T23:00:00.000Z", + "key": 1448924400000, <1> + "doc_count": 330 + }, ... +---- + +<1> `"key"` — The unix timestamp you can use without conversions by the +Vega date expressions. + +For most visualizations, you only need the list of bucket values. To focus on +only the data you need, use `format: {property: "aggregations.time_buckets.buckets"}`. + +Specify a query with individual range and dashboard context. The query is +equivalent to `"%context%": true, "%timefield%": "@timestamp"`, +except that the time range is shifted back by 10 minutes: + +[source,yaml] +---- +{ + body: { + query: { + bool: { + must: [ + // This string will be replaced + // with the auto-generated "MUST" clause + "%dashboard_context-must_clause%" + { + range: { + // apply timefilter (upper right corner) + // to the @timestamp variable + @timestamp: { + // "%timefilter%" will be replaced with + // the current values of the time filter + // (from the upper right corner) + "%timefilter%": true + // Only work with %timefilter% + // Shift current timefilter by 10 units back + shift: 10 + // week, day (default), hour, minute, second + unit: minute + } + } + } + ] + must_not: [ + // This string will be replaced with + // the auto-generated "MUST-NOT" clause + "%dashboard_context-must_not_clause%" + ] + filter: [ + // This string will be replaced + // with the auto-generated "FILTER" clause + "%dashboard_context-filter_clause%" + ] + } + } + } +} +---- + +NOTE: When using `"%context%": true` or defining a value for `"%timefield%"` the body cannot contain a query. To customize the query within the VEGA specification (e.g. add an additional filter, or shift the timefilter), define your query and use the placeholders as in the example above. The placeholders will be replaced by the actual context of the dashboard or visualization once parsed. + +The `"%timefilter%"` can also be used to specify a single min or max +value. The date_histogram's `extended_bounds` can be set +with two values - min and max. Instead of hardcoding a value, you may +use `"min": {"%timefilter%": "min"}`, which will be replaced with the +beginning of the current time range. The `shift` and `unit` values are +also supported. The `"interval"` can also be set dynamically, depending +on the currently picked range: `"interval": {"%autointerval%": 10}` will +try to get about 10-15 data points (buckets). + +[float] +[[vega-esmfiles]] +=== Access Elastic Map Service files + +experimental[] Access the Elastic Map Service files via the same mechanism: + +[source,yaml] +---- +url: { + // "type" defaults to "elasticsearch" otherwise + type: emsfile + // Name of the file, exactly as in the Region map visualization + name: World Countries +} +// The result is a geojson file, get its features to use +// this data source with the "shape" marks +// https://vega.github.io/vega/docs/marks/shape/ +format: {property: "features"} +---- + +To enable Maps, the graph must specify `type=map` in the host +configuration: + +[source,yaml] +---- +{ + "config": { + "kibana": { + "type": "map", + + // Initial map position + "latitude": 40.7, // default 0 + "longitude": -74, // default 0 + "zoom": 7, // default 2 + + // defaults to "default". Use false to disable base layer. + "mapStyle": false, + + // default 0 + "minZoom": 5, + + // defaults to the maximum for the given style, + // or 25 when base is disabled + "maxZoom": 13, + + // defaults to true, shows +/- buttons to zoom in/out + "zoomControl": false, + + // Defaults to 'false', disables mouse wheel zoom. If set to + // 'true', map may zoom unexpectedly while scrolling dashboard + "scrollWheelZoom": false, + + // When false, repaints on each move frame. + // Makes the graph slower when moving the map + "delayRepaint": true, // default true + } + }, + /* the rest of Vega JSON */ +} +---- + +The visualization automatically injects a `"projection"`, which you can use to +calculate the position of all geo-aware marks. +Additionally, you can use `latitude`, `longitude`, and `zoom` signals. +These signals can be used in the graph, or can be updated to modify the +position of the map. + +[float] +[[vega-tooltip]] +==== Additional tooltip styling + +{kib} has installed the https://vega.github.io/vega-lite/docs/tooltip.html[Vega tooltip plugin], +so tooltips can be defined in the ways documented there. Beyond that, {kib} also supports +a configuration option for changing the tooltip position and padding: + +```js +{ + config: { + kibana: { + tooltips: { + position: 'top', + padding: 15 + } + } + } +} +``` + +[float] +[[vega-url-loading]] +==== Advanced setting to enable URL loading from any domain + +Vega can load data from any URL, but this is disabled by default in {kib}. +To change this, set `vis_type_vega.enableExternalUrls: true` in `kibana.yml`, +then restart {kib}. + +[float] +[[vega-inspector]] +==== Vega Inspector +Use the contextual *Inspect* tool to gain insights into different elements. +For Vega visualizations, there are two different views: *Request* and *Vega debug*. + +[float] +[[inspect-elasticsearch-requests]] +===== Inspect {es} requests + +Vega uses the {ref}/search-search.html[{es} search API] to get documents and aggregation +results from {es}. To troubleshoot these requests, click *Inspect*, which shows the most recent requests. +In case your specification has more than one request, you can switch between the views using the *View* dropdown. + +[role="screenshot"] +image::visualize/images/vega_tutorial_inspect_requests.png[] + +[float] +[[vega-debugging]] +===== Vega debugging + +With the *Vega debug* view, you can inspect the *Data sets* and *Signal Values* runtime data. + +The runtime data is read from the +https://vega.github.io/vega/docs/api/debugging/#scope[runtime scope]. + +[role="screenshot"] +image::visualize/images/vega_tutorial_inspect_data_sets.png[] + +To debug more complex specs, access to the `view` variable. For more information, refer to +the <>. + +[float] +[[asking-for-help-with-a-vega-spec]] +===== Asking for help with a Vega spec + +Because of the dynamic nature of the data in {es}, it is hard to help you with +Vega specs unless you can share a dataset. To do this, click *Inspect*, select the *Vega debug* view, +then select the *Spec* tab: + +[role="screenshot"] +image::visualize/images/vega_tutorial_getting_help.png[] + +To copy the response, click *Copy to clipboard*. Paste the copied data to +https://gist.github.com/[gist.github.com], possibly with a .json extension. Use the [raw] button, +and share that when asking for help. + +[float] +[[vega-browser-debugging-console]] +==== Browser debugging console + +experimental[] Use browser debugging tools (for example, F12 or Ctrl+Shift+J in Chrome) to +inspect the `VEGA_DEBUG` variable: + +* `view` — Access to the Vega View object. See https://vega.github.io/vega/docs/api/debugging/[Vega Debugging Guide] +on how to inspect data and signals at runtime. For Vega-Lite, +`VEGA_DEBUG.view.data('source_0')` gets the pre-transformed data, and `VEGA_DEBUG.view.data('data_0')` +gets the encoded data. For Vega, it uses the data name as defined in your Vega spec. + +* `vega_spec` — Vega JSON graph specification after some modifications by {kib}. In case +of Vega-Lite, this is the output of the Vega-Lite compiler. + +* `vegalite_spec` — If this is a Vega-Lite graph, JSON specification of the graph before +Vega-Lite compilation. + +[float] +[[vega-expression-functions]] +==== (Vega only) Expression functions which can update the time range and dashboard filters + +{kib} has extended the Vega expression language with these functions: + +```js +/** + * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor + * @param {string} [index] as defined in Kibana, or default if missing + */ +kibanaAddFilter(query, index) + +/** + * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor + * @param {string} [index] as defined in Kibana, or default if missing + */ +kibanaRemoveFilter(query, index) + +kibanaRemoveAllFilters() + +/** + * Update dashboard time filter to the new values + * @param {number|string|Date} start + * @param {number|string|Date} end + */ +kibanaSetTimeFilter(start, end) +``` + +[float] +[[vega-additional-configuration-options]] +==== Additional configuration options + +[source,yaml] +---- +{ + config: { + kibana: { + // Placement of the Vega-defined signal bindings. + // Can be `left`, `right`, `top`, or `bottom` (default). + controlsLocation: top + // Can be `vertical` or `horizontal` (default). + controlsDirection: vertical + // If true, hides most of Vega and Vega-Lite warnings + hideWarnings: true + // Vega renderer to use: `svg` or `canvas` (default) + renderer: canvas + } + } +} +---- + +[[vega-notes]] +[[resources-and-examples]] +=== Resources and examples + +experimental[] To learn more about Vega and Vega-Lite, refer to the resources and examples. + +[float] +[[vega-editor]] +==== Vega editor +The https://vega.github.io/editor/[Vega Editor] includes examples for Vega & Vega-Lite, but does not support any +{kib}-specific features like {es} requests and interactive base maps. + +[float] +[[vega-lite-resources]] +==== Vega-Lite resources +* https://vega.github.io/vega-lite/tutorials/getting_started.html[Tutorials] +* https://vega.github.io/vega-lite/docs/[Docs] +* https://vega.github.io/vega-lite/examples/[Examples] + +[float] +[[vega-resources]] +==== Vega resources +* https://vega.github.io/vega/tutorials/[Tutorials] +* https://vega.github.io/vega/docs/[Docs] +* https://vega.github.io/vega/examples/[Examples] + +TIP: When you use the examples in {kib}, you may +need to modify the "data" section to use absolute URL. For example, +replace `"url": "data/world-110m.json"` with +`"url": "https://vega.github.io/editor/data/world-110m.json"`. diff --git a/docs/user/getting-started.asciidoc b/docs/user/getting-started.asciidoc index 2ff3a09152df4..a877f6a66a79a 100644 --- a/docs/user/getting-started.asciidoc +++ b/docs/user/getting-started.asciidoc @@ -1,19 +1,19 @@ -[[getting-started]] +[[get-started]] = Get started [partintro] -- -Ready to try out {kib} and see what it can do? To quickest way to get started with {kib} is to set up on Cloud, then add a sample data set that helps you get a handle on the full range of {kib} features. +Ready to try out {kib} and see what it can do? The quickest way to get started with {kib} is to set up on Cloud, then add a sample data set to explore the full range of {kib} features. [float] -[[cloud-set-up]] +[[set-up-on-cloud]] == Set up on cloud include::{docs-root}/shared/cloud/ess-getting-started.asciidoc[] [float] -[[get-data-in]] +[[gs-get-data-into-kibana]] == Get data into {kib} The easiest way to get data into {kib} is to add a sample data set. @@ -42,12 +42,11 @@ NOTE: The timestamps in the sample data sets are relative to when they are insta If you uninstall and reinstall a data set, the timestamps change to reflect the most recent installation. [float] -[[getting-started-next-steps]] == Next steps -* To get a hands-on experience creating visualizations, follow the <> tutorial. +* To get a hands-on experience creating visualizations, follow the <> tutorial. -* If you're ready to load an actual data set and build a dashboard, follow the <> tutorial. +* If you're ready to load an actual data set and build a dashboard, follow the <> tutorial. -- @@ -60,5 +59,3 @@ include::{kib-repo-dir}/getting-started/tutorial-define-index.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-discovering.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-visualizing.asciidoc[] - -include::{kib-repo-dir}/getting-started/tutorial-dashboard.asciidoc[] diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index abbdbeb68d9cb..e909626c5779c 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -2,8 +2,6 @@ include::introduction.asciidoc[] include::whats-new.asciidoc[] -include::getting-started.asciidoc[] - include::setup.asciidoc[] include::monitoring/configuring-monitoring.asciidoc[leveloffset=+1] @@ -13,9 +11,11 @@ include::monitoring/monitoring-kibana.asciidoc[leveloffset=+2] include::security/securing-kibana.asciidoc[] +include::getting-started.asciidoc[] + include::discover.asciidoc[] -include::dashboard.asciidoc[] +include::dashboard/dashboard.asciidoc[] include::canvas.asciidoc[] @@ -25,8 +25,6 @@ include::ml/index.asciidoc[] include::graph/index.asciidoc[] -include::visualize.asciidoc[] - include::{kib-repo-dir}/observability/index.asciidoc[] include::{kib-repo-dir}/logs/index.asciidoc[] diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index ff936fb4d5569..079d183dd959d 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -83,12 +83,6 @@ image::images/intro-dashboard.png[] {kib} also offers these visualization features: -* <> allows you to display your data in -charts, graphs, and tables -(just to name a few). It's also home to Lens. -Visualize supports the ability to add interactive -controls to your dashboard, filter dashboard content in real time, and add your own images and logos for your brand. - * <> gives you the ability to present your data in a visually compelling, pixel-perfect report. Give your data the “wow” factor needed to impress your CEO or to captivate coworkers with a big-screen display. @@ -98,7 +92,7 @@ questions of your location-based data. Maps supports multiple layers and data sources, mapping of individual geo points and shapes, and dynamic client-side styling. -* <> allows you to combine +* <> allows you to combine an infinite number of aggregations to display complex data. With TSVB, you can analyze multiple index patterns and customize every aspect of your visualization. Choose your own date format and color @@ -161,6 +155,6 @@ and start exploring data in minutes. You can also <> — no code, no additional infrastructure required. -Our <> and in-product guidance can +Our <> and in-product guidance can help you get up and running, faster. Click the help icon image:images/intro-help-icon.png[] in the top navigation bar for help with questions or to provide feedback. diff --git a/docs/user/reporting/automating-report-generation.asciidoc b/docs/user/reporting/automating-report-generation.asciidoc index 3e227229ddcc5..371855deb2f3c 100644 --- a/docs/user/reporting/automating-report-generation.asciidoc +++ b/docs/user/reporting/automating-report-generation.asciidoc @@ -13,7 +13,7 @@ URL that triggers a report to generate. To create the POST URL for PDF reports: -. Go to *Visualize* or *Dashboard*, then open the visualization or dashboard. +. Go to *Dashboard*, then open the visualization or dashboard. + To specify a relative or absolute time period, use the time filter. diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index 4f4d59315fafa..50ae92382fb24 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -11,7 +11,7 @@ saved search, or Canvas workpad. Depending on the object type, you can export th a PDF, PNG, or CSV document, which you can keep for yourself, or share with others. Reporting is available from the *Share* menu -in *Discover*, *Visualize*, *Dashboard*, and *Canvas*. +in *Discover*, *Dashboard*, and *Canvas*. [role="screenshot"] image::user/reporting/images/share-button.png["Share"] diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc index 3a4b2202201e2..cc4af9041bcd9 100644 --- a/docs/user/security/rbac_tutorial.asciidoc +++ b/docs/user/security/rbac_tutorial.asciidoc @@ -28,7 +28,7 @@ To complete this tutorial, you'll need the following: * **A space**: In this tutorial, use `Dev Mortgage` as the space name. See <> for details on creating a space. -* **Data**: You can use <> or +* **Data**: You can use <> or live data. In the following steps, Filebeat and Metricbeat data are used. [float] diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index 4e02759ce99cb..daf9720a0f1d8 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -47,7 +47,7 @@ image::user/security/images/reporting-privileges-example.png["Reporting privileg Reporting users typically save searches, create visualizations, and build dashboards. They require a space that provides read and write privileges in -*Discover*, *Visualize*, and *Dashboard*. +*Discover* and *Dashboard*. . Save your new role. diff --git a/docs/user/visualize.asciidoc b/docs/user/visualize.asciidoc deleted file mode 100644 index dc116962f9e96..0000000000000 --- a/docs/user/visualize.asciidoc +++ /dev/null @@ -1,142 +0,0 @@ -[[visualize]] -= Visualize - -[partintro] --- -_Visualize_ enables you to create visualizations of the data from your {es} indices, which you can then add to dashboards for analysis. - -{kib} visualizations are based on {es} queries. By using a series of {es} {ref}/search-aggregations.html[aggregations] to extract and process your data, you can create charts that show you the trends, spikes, and dips you need to know about. - -To begin, open the menu, go to *Visualize*, then click *Create visualization*. - -[float] -[[visualization-types]] -== Types of visualizations - -{kib} supports several types of visualizations. - -<>:: -Quickly build several types of basic visualizations by simply dragging and dropping the data fields you want to display. - -<>:: - -* *Line, area, and bar charts* — Compares different series in X/Y charts. - -* *Pie chart* — Displays each source contribution to a total. - -* *Data table* — Flattens aggregations into table format. - -* *Metric* — Displays a single number. - -* *Goal and gauge* — Displays a number with progress indicators. - -* *Tag cloud* — Displays words in a cloud, where the size of the word corresponds to its importance. - -<>:: Visualizes time series data using pipeline aggregations. - -<>:: Computes and combine data from multiple time series -data sets. - -<>:: -* *<>* — Displays geospatial data in {kib}. - -* <>:: Display shaded cells within a matrix. - -<>:: - -* *Markdown widget* — Displays free-form information or instructions. - -* *Controls* — Adds interactive inputs to a dashboard. - -<>:: Completes control over query and display. - -[float] -[[choose-your-data]] -== Choose your data - -Specify a search query to retrieve the data for your visualization, or used rolled up data. - -* To enter new search criteria, select the <> for the indices that -contain the data you want to visualize. The visualization builder opens -with a wildcard query that matches all of the documents in the selected -indices. - -* To build a visualization from a saved search, click the name of the saved -search you want to use. The visualization builder opens and loads the -selected query. -+ -NOTE: When you build a visualization from a saved search, any subsequent -modifications to the saved search are reflected in the -visualization. To disable automatic updates, delete the visualization -on the *Saved Object* page. - -* To build a visualization using <>, select -the index pattern that includes the data. Rolled up data is summarized into -time buckets that can be split into sub buckets for numeric field values or -terms. To lower granularity, use a time aggregation that uses and combines -several time buckets. For an example, refer to <>. - -[float] -[[vis-inspector]] -== Inspect visualizations - -Many visualizations allow you to inspect the query and data behind the visualization. - -. In the {kib} toolbar, click *Inspect*. -. To download the data, click *Download CSV*, then choose one of the following options: -* *Formatted CSV* - Downloads the data in table format. -* *Raw CSV* - Downloads the data as provided. -. To view the requests for collecting data, select *Requests* from the *View* -dropdown. - -[float] -[[save-visualize]] -== Save visualizations -To use your visualizations in <>, you must save them. - -. In the {kib} toolbar, click *Save*. -. Enter the visualization *Title* and optional *Description*, then *Save* the visualization. - -To access the saved visualization, go to *Management > {kib} > Saved Objects*. - -[float] -[[save-visualization-read-only-access]] -==== Read only access -When you have insufficient privileges to save visualizations, the following indicator is -displayed and the *Save* button is not visible. - -For more information, refer to <>. - -[role="screenshot"] -image::visualize/images/read-only-badge.png[Example of Visualize's read only access indicator in Kibana's header] - -[float] -[[visualize-share-options]] -== Share visualizations - -When you've finished your visualization, you can share it outside of {kib}. - -From the *Share* menu, you can: - -* Embed the code in a web page. Users must have {kib} access -to view an embedded visualization. -* Share a direct link to a {kib} visualization. -* Generate a PDF report. -* Generate a PNG report. - --- -include::{kib-repo-dir}/visualize/aggregations.asciidoc[] - -include::{kib-repo-dir}/visualize/lens.asciidoc[] - -include::{kib-repo-dir}/visualize/most-frequent.asciidoc[] - -include::{kib-repo-dir}/visualize/tsvb.asciidoc[] - -include::{kib-repo-dir}/visualize/timelion.asciidoc[] - -include::{kib-repo-dir}/visualize/tilemap.asciidoc[] - -include::{kib-repo-dir}/visualize/for-dashboard.asciidoc[] - -include::{kib-repo-dir}/visualize/vega.asciidoc[] diff --git a/docs/visualize/aggregations.asciidoc b/docs/visualize/aggregations.asciidoc deleted file mode 100644 index ef38f716f2303..0000000000000 --- a/docs/visualize/aggregations.asciidoc +++ /dev/null @@ -1,110 +0,0 @@ -[[supported-aggregations]] -== Supported aggregations - -Use the supported aggregations to build your visualizations. - -[float] -[[visualize-metric-aggregations]] -=== Metric aggregations - -Metric aggregations extract field from documents to generate data values. - -{ref}/search-aggregations-metrics-avg-aggregation.html[Average]:: The mean value. - -{ref}/search-aggregations-metrics-valuecount-aggregation.html[Count]:: The total number of documents that match the query, which allows you to visualize the number of documents in a bucket. Count is the default value. - -{ref}/search-aggregations-metrics-max-aggregation.html[Max]:: The highest value. - -{ref}/search-aggregations-metrics-percentile-aggregation.html[Median]:: The value that is in the 50% percentile. - -{ref}/search-aggregations-metrics-min-aggregation.html[Min]:: The lowest value. - -{ref}/search-aggregations-metrics-percentile-rank-aggregation.html[Percentile ranks]:: Returns the percentile rankings for the values in the specified numeric field. Select a numeric field from the drop-down, then specify one or more percentile rank values in the *Values* fields. - -{ref}/search-aggregations-metrics-percentile-aggregation.html[Percentiles]:: Divides the -values in a numeric field into specified percentile bands. Select a field from the drop-down, then specify one or more ranges in the *Percentiles* fields. - -Standard Deviation:: Requires a numeric field. Uses the {ref}/search-aggregations-metrics-extendedstats-aggregation.html[_extended stats_] aggregation. - -{ref}/search-aggregations-metrics-sum-aggregation.html[Sum]:: The total value. - -{ref}/search-aggregations-metrics-top-hits-aggregation.html[Top hit]:: Returns a sample of individual documents. When the Top Hit aggregation is matched to more than one document, you must choose a technique for combining the values. Techniques include average, minimum, maximum, and sum. - -Unique Count:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[Cardinality] of the field within the bucket. - -Alternatively, you can override the field values with a script using JSON input. For example: - -[source,shell] -{ "script" : "doc['grade'].value * 1.2" } - -The example implements a {es} {ref}/search-aggregations.html[Script Value Source], which replaces -the value in the metric. The options available depend on the aggregation you choose. - -[float] -[[visualize-parent-pipeline-aggregations]] -=== Parent pipeline aggregations - -Parent pipeline aggregations assume the bucket aggregations are ordered and are especially useful for time series data. For each parent pipeline aggregation, you must define a bucket aggregation and metric aggregation. - -You can also nest these aggregations. For example, if you want to produce a third derivative. - -{ref}/search-aggregations-pipeline-bucket-script-aggregation.html[Bucket script]:: Executes a script that performs computations for each bucket that specifies metrics in the parent multi-bucket aggregation. - -{ref}/search-aggregations-pipeline-cumulative-sum-aggregation.html[Cumulative sum]:: Calculates the cumulative sum of a specified metric in a parent histogram. - -{ref}/search-aggregations-pipeline-derivative-aggregation.html[Derivative]:: Calculates the derivative of specific metrics. - -{ref}/search-aggregations-pipeline-movavg-aggregation.html[Moving avg]:: Slides a window across the data and emits the average value of the window. - -{ref}/search-aggregations-pipeline-serialdiff-aggregation.html[Serial diff]:: Values in a time series are subtracted from itself at different time lags or periods. - -[float] -[[visualize-sibling-pipeline-aggregations]] -=== Sibling pipeline aggregations - -Sibling pipeline aggregations condense many buckets into one. For each sibling pipeline aggregation, you must define a bucket aggregations and metric aggregation. - -{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Average bucket]:: Calculates the mean, or average, value of a specified metric in a sibling aggregation. - -{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Max Bucket]:: Calculates the maximum value of a specified metric in a sibling aggregation. - -{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Min Bucket]:: Calculates the minimum value of a specified metric in a sibling aggregation. - -{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Sum Bucket]:: Calculates the sum of the values of a specified metric in a sibling aggregation. - -[float] -[[visualize-bucket-aggregations]] -=== Bucket aggregations - -Bucket aggregations sort documents into buckets, depending on the contents of the document. - -{ref}/search-aggregations-bucket-datehistogram-aggregation.html[Date histogram]:: Splits a date field into buckets by interval. If the date field is the primary time field for the index pattern, it chooses an automatic interval for you. Intervals are labeled at the start of the interval, using the date-key returned by {es}. For example, the tooltip for a monthly interval displays the first day of the month. - -{ref}/search-aggregations-bucket-daterange-aggregation.html[Date range]:: Reports values that are within a range of dates that you specify. You can specify the ranges for the dates using {ref}/common-options.html#date-math[_date math_] expressions. - -{ref}/search-aggregations-bucket-filter-aggregation.html[Filter]:: Each filter creates a bucket of documents. You can specify a filter as a -<> or <> query string. - -{ref}/search-aggregations-bucket-geohashgrid-aggregation.html[Geohash]:: Displays points based on a geohash. Supported by data table visualizations and <>. - -{ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile]:: Groups points based on web map tiling. Supported by data table visualizations and <>. - -{ref}/search-aggregations-bucket-histogram-aggregation.html[Histogram]:: Builds from a numeric field. - -{ref}/search-aggregations-bucket-iprange-aggregation.html[IPv4 range]:: Specify ranges of IPv4 addresses. - -{ref}/search-aggregations-bucket-range-aggregation.html[Range]:: Specify ranges of values for a numeric field. - -{ref}/search-aggregations-bucket-significantterms-aggregation.html[Significant terms]:: Returns interesting or unusual occurrences of terms in a set. Supports {es} {ref}/search-aggregations-bucket-terms-aggregation.html#_filtering_values_4[exclude and include patterns]. - -{ref}/search-aggregations-bucket-terms-aggregation.html[Terms]:: Specify the top or bottom _n_ elements of a given field to display, ordered by count or a custom metric. Supports {es} {ref}/search-aggregations-bucket-terms-aggregation.html#_filtering_values_4[exclude and include patterns]. - -{kib} filters string fields with only regular expression patterns, and does not filter numeric fields or match with arrays. - -For example: - -* You want to exclude the metricbeat process from your visualization of top processes: `metricbeat.*` -* You only want to show processes collecting beats: `.*beat` -* You want to exclude two specific values, the string `"empty"` and `"none"`: `empty|none` - -Patterns are case sensitive. diff --git a/docs/visualize/for-dashboard.asciidoc b/docs/visualize/for-dashboard.asciidoc deleted file mode 100644 index 400179e9ceae7..0000000000000 --- a/docs/visualize/for-dashboard.asciidoc +++ /dev/null @@ -1,67 +0,0 @@ -[[for-dashboard]] -== Dashboard tools - -Visualize comes with controls and Markdown tools that you can add to dashboards for an interactive experience. - -[float] -[[controls]] -=== Controls -experimental[] - -The controls tool enables you to add interactive inputs -on a dashboard. - -You can add two types of interactive inputs: - -* *Options list* — Filters content based on one or more specified options. The dropdown menu is dynamically populated with the results of a terms aggregation. For example, use the options list on the sample flight dashboard when you want to filter the data by origin city and destination city. - -* *Range slider* — Filters data within a specified range of numbers. The minimum and maximum values are dynamically populated with the results of a min and max aggregation. For example, use the range slider when you want to filter the sample flight dashboard by a specific average ticket price. - -[role="screenshot"] -image::images/dashboard-controls.png[] - -[float] -[[controls-options]] -==== Controls options - -Configure the settings that apply to the interactive inputs on a dashboard. - -. Click *Options*, then configure the following: - -* *Update {kib} filters on each change* — When selected, all interactive inputs create filters that refresh the dashboard. When unselected, {kib} filters are created only when you click *Apply changes*. - -* *Use time filter* — When selected, the aggregations that generate the options list and time range are connected to the <>. - -* *Pin filters to global state* — When selected, all filters created by interacting with the inputs are automatically pinned. - -. Click *Update*. - -[float] -[[markdown-widget]] -=== Markdown - -The Markdown tool is a text entry field that accepts GitHub-flavored Markdown text. When you enter the text, the tool populates the results on the dashboard. - -Markdown is helpful when you want to include important information, instructions, and images on your dashboard. - -For information about GitHub-flavored Markdown text, click *Help*. - -For example, when you enter: - -[role="screenshot"] -image::images/markdown_example_1.png[] - -The following instructions are displayed: - -[role="screenshot"] -image::images/markdown_example_2.png[] - -Or when you enter: - -[role="screenshot"] -image::images/markdown_example_3.png[] - -The following image is displayed: - -[role="screenshot"] -image::images/markdown_example_4.png[] diff --git a/docs/visualize/images/lens_drag_drop.gif b/docs/visualize/images/lens_drag_drop.gif index ca62115e7ea3a..1f8580d462702 100644 Binary files a/docs/visualize/images/lens_drag_drop.gif and b/docs/visualize/images/lens_drag_drop.gif differ diff --git a/docs/visualize/lens.asciidoc b/docs/visualize/lens.asciidoc deleted file mode 100644 index 6e51433bca3f6..0000000000000 --- a/docs/visualize/lens.asciidoc +++ /dev/null @@ -1,173 +0,0 @@ -[role="xpack"] -[[lens]] -== Lens - -beta[] - -*Lens* is a simple and fast way to create visualizations of your {es} data. To create visualizations, -you drag and drop your data fields onto the visualization builder pane, and *Lens* automatically generates -a visualization that best displays your data. - -With Lens, you can: - -* Use the automatically generated visualization suggestions to change the visualization type. - -* Create visualizations with multiple layers and indices. - -* Add your visualizations to dashboards and Canvas workpads. - -To get started with *Lens*, select a field in the data panel, then drag and drop the field on a highlighted area. - -[role="screenshot"] -image::images/lens_drag_drop.gif[Drag and drop] - -You can incorporate many fields into your visualization, and Lens uses heuristics to decide how to apply each one to the visualization. - -TIP: Drag-and-drop capabilities are available only when Lens knows how to use the data. If *Lens* is unable to automatically generate a visualization, -you can still configure the customization options for your visualization. - -[float] -[[apply-lens-filters]] -==== Change the data panel fields - -The fields in the data panel are based on the selected <> and <>. - -To change the index pattern, click it, then select a new one. The fields in the data panel automatically update. - -To filter the fields in the data panel: - -* Enter the name in *Search field names*. - -* Click *Filter by type*, then select the filter. To show all of the fields in the index pattern, deselect *Only show fields with data*. - -[float] -[[view-data-summaries]] -==== Data summaries - -To help you decide exactly the data you want to display, get a quick summary of each field. The summary shows the distribution of values within the selected time range. - -To view the field summary information, navigate to the field, then click *i*. - -[role="screenshot"] -image::images/lens_data_info.png[Data summary window] - -[float] -[[change-the-visualization-type]] -==== Change the visualization type - -*Lens* enables you to switch between any supported visualization type at any time. - -*Suggestions* are shortcuts to alternate visualizations that *Lens* generates for you. - -[role="screenshot"] -image::images/lens_suggestions.gif[Visualization suggestions] - -If you'd like to use a visualization type that is not suggested, click the visualization type, -then select a new one. - -[role="screenshot"] -image::images/lens_viz_types.png[] - -When there is an exclamation point (!) -next to a visualization type, Lens is unable to transfer your data, but -still allows you to make the change. - -[float] -[[customize-operation]] -==== Change the aggregation and labels - -For each visualization, Lens allows some customizations of the data. - -. Click *Drop a field here* or the field name in the column. - -. Change the options that appear. Options vary depending on the type of field. -+ -[role="screenshot"] -image::images/lens_aggregation_labels.png[Quick function options] - -[float] -[[layers]] -==== Add layers and indices - -Area, line, and bar charts allow you to visualize multiple data layers and indices so that you can compare and analyze data from multiple sources. - -To add a layer, click *+*, then drag and drop the fields for the new layer. - -[role="screenshot"] -image::images/lens_layers.png[Add layers] - -To view a different index, click it, then select a new one. - -[role="screenshot"] -image::images/lens_index_pattern.png[Add index pattern] - -[float] -[[lens-tutorial]] -=== Lens tutorial - -Ready to create your own visualization with Lens? Use the following tutorial to create a visualization that -lets you compare sales over time. - -[float] -[[lens-before-begin]] -==== Before you begin - -To start, you'll need to add the <>. - -[float] -==== Build the visualization - -Drag and drop your data onto the visualization builder pane. - -. Select the *kibana_sample_data_ecommerce* index pattern. - -. Click image:images/time-filter-calendar.png[], then click *Last 7 days*. -+ -The fields in the data panel update. - -. Drag and drop the *taxful_total_price* data field to the visualization builder pane. -+ -[role="screenshot"] -image::images/lens_tutorial_1.png[Lens tutorial] - -To display the average order prices over time, *Lens* automatically added in *order_date* field. - -To break down your data, drag the *category.keyword* field to the visualization builder pane. Lens -knows that you want to show the top categories and compare them across the dates, -and creates a chart that compares the sales for each of the top three categories: - -[role="screenshot"] -image::images/lens_tutorial_2.png[Lens tutorial] - -[float] -[[customize-lens-visualization]] -==== Customize your visualization - -Make your visualization look exactly how you want with the customization options. - -. Click *Average of taxful_total_price*, then change the *Label* to `Sales`. -+ -[role="screenshot"] -image::images/lens_tutorial_3.1.png[Lens tutorial] - -. Click *Top values of category.keyword*, then change *Number of values* to `10`. -+ -[role="screenshot"] -image::images/lens_tutorial_3.2.png[Lens tutorial] -+ -The visualization updates to show there are only six available categories. -+ -Look at the *Suggestions*. An area chart is not an option, but for the sales data, a stacked area chart might be the best option. - -. To switch the chart type, click *Stacked bar chart* in the column, then click *Stacked area* from the *Select a visualizations* window. -+ -[role="screenshot"] -image::images/lens_tutorial_3.png[Lens tutorial] - -[float] -[[lens-tutorial-next-steps]] -==== Next steps - -Now that you've created your visualization, you can add it to a dashboard or Canvas workpad. - -For more information, refer to <> or <>. diff --git a/docs/visualize/most-frequent.asciidoc b/docs/visualize/most-frequent.asciidoc deleted file mode 100644 index f716930e7e65c..0000000000000 --- a/docs/visualize/most-frequent.asciidoc +++ /dev/null @@ -1,59 +0,0 @@ -[[most-frequent]] -== Most frequently used visualizations - -The most frequently used visualizations allow you to plot aggregated data from a <> or <>. - -The most frequently used visualizations include: - -* Line, area, and bar charts -* Pie chart -* Data table -* Metric, goal, and gauge -* Tag cloud - -[[metric-chart]] - -[float] -=== Configure your visualization - -You configure visualizations using the default editor. Each visualization supports different configurations of the metrics and buckets. - -For example, a bar chart allows you to add an x-axis: - -[role="screenshot"] -image::images/add-bucket.png["",height=478] - -A common configuration for the x-axis is to use a {es} {ref}/search-aggregations-bucket-datehistogram-aggregation.html[date histogram] aggregation: - -[role="screenshot"] -image::images/visualize-date-histogram.png[] - -To see your changes, click *Apply changes* image:images/apply-changes-button.png[] - -If it's supported by the visualization, you can add more buckets. In this example we have -added a -{es} {ref}/search-aggregations-bucket-terms-aggregation.html[terms] aggregation on the field -`geo.src` to show the top 5 sources of log traffic. - -[role="screenshot"] -image::images/visualize-date-histogram-split-1.png[] - -The new aggregation is added after the first one, so the result shows -the top 5 sources of traffic per 3 hours. If you want to change the aggregation order, you can do -so by dragging: - -[role="screenshot"] -image::images/visualize-drag-reorder.png["",width=366] - -The visualization -now shows the top 5 sources of traffic overall, and compares them in 3 hour increments: - -[role="screenshot"] -image::images/visualize-date-histogram-split-2.png[] - -For more information about how aggregations are used in visualizations, see <>. - -Each visualization also has its own customization options. Most visualizations allow you to customize the color of a specific series: - -[role="screenshot"] -image::images/color-picker.png[An array of color dots that users can select,height=267] diff --git a/docs/visualize/tilemap.asciidoc b/docs/visualize/tilemap.asciidoc deleted file mode 100644 index c889bd0bb6ca0..0000000000000 --- a/docs/visualize/tilemap.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[[heat-map]] -== Heat map - -Display graphical representations of data where the individual values are represented by colors. Use heat maps when your data set includes categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data. - -[role="screenshot"] -image::images/visualize_heat_map_example.png[] - -[float] -[[navigate-heatmap]] -=== Change the color ranges - -When only one color displays on the heat map, you might need to change the color ranges. - -To specify the number of color ranges: - -. Click *Options*. - -. Enter the *Number of colors* to display. - -To specify custom ranges: - -. Click *Options*. - -. Select *Use custom ranges*. - -. Enter the ranges to display. diff --git a/docs/visualize/timelion.asciidoc b/docs/visualize/timelion.asciidoc deleted file mode 100644 index 4869664fab0a4..0000000000000 --- a/docs/visualize/timelion.asciidoc +++ /dev/null @@ -1,547 +0,0 @@ -[[timelion]] -== Timelion - -Timelion is a time series data visualizer that enables you to combine totally -independent data sources within a single visualization. It's driven by a simple -expression language you use to retrieve time series data, perform calculations -to tease out the answers to complex questions, and visualize the results. - -For example, Timelion enables you to easily get the answers to questions like: - -* <> -* <> -* <> - -[float] -[[time-series-before-you-begin]] -=== Before you begin - -In this tutorial, you'll use the time series data from https://www.elastic.co/guide/en/beats/metricbeat/current/index.html[Metricbeat]. To ingest the data locally, link:https://www.elastic.co/downloads/beats/metricbeat[download Metricbeat]. - -[float] -[[time-series-intro]] -=== Create time series visualizations - -To compare the real-time percentage of CPU time spent in user space to the results offset by one hour, create a time series visualization. - -[float] -[[time-series-define-functions]] -==== Define the functions - -To start tracking the real-time percentage of CPU, enter the following in the *Timelion Expression* field: - -[source,text] ----------------------------------- -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') ----------------------------------- - -[role="screenshot"] -image::images/timelion-create01.png[] -{nbsp} - -[float] -[[time-series-compare-data]] -==== Compare the data - -To compare the two data sets, add another series with data from the previous hour, separated by a comma: - -[source,text] ----------------------------------- -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct'), -.es(offset=-1h, <1> - index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') ----------------------------------- - -<1> `offset` offsets the data retrieval by a date expression. In this example, `-1h` offsets the data back by one hour. - -[role="screenshot"] -image::images/timelion-create02.png[] -{nbsp} - -[float] -[[time-series-add-labels]] -==== Add label names - -To easily distinguish between the two data sets, add the label names: - -[source,text] ----------------------------------- -.es(offset=-1h,index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct').label('last hour'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct').label('current hour') <1> ----------------------------------- - -<1> `.label()` adds custom labels to the visualization. - -[role="screenshot"] -image::images/timelion-create03.png[] -{nbsp} - -[float] -[[time-series-title]] -==== Add a title - -Add a meaningful title: - -[source,text] ----------------------------------- -.es(offset=-1h, - index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('last hour'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('current hour') - .title('CPU usage over time') <1> ----------------------------------- - -<1> `.title()` adds a title with a meaningful name. Titles make is easier for unfamiliar users to understand the purpose of the visualization. - -[role="screenshot"] -image::images/timelion-customize01.png[] -{nbsp} - -[float] -[[time-series-change-chart-type]] -==== Change the chart type - -To differentiate between the current hour data and the last hour data, change the chart type: - -[source,text] ----------------------------------- -.es(offset=-1h, - index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('last hour') - .lines(fill=1,width=0.5), <1> -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('current hour') - .title('CPU usage over time') ----------------------------------- - -<1> `.lines()` changes the appearance of the chart lines. In this example, `.lines(fill=1,width=0.5)` sets the fill level to `1`, and the border width to `0.5`. - -[role="screenshot"] -image::images/timelion-customize02.png[] -{nbsp} - -[float] -[[time-series-change-color]] -==== Change the line colors - -To make the current hour data stand out, change the line colors: - -[source,text] ----------------------------------- -.es(offset=-1h, - index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('last hour') - .lines(fill=1,width=0.5) - .color(gray), <1> -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('current hour') - .title('CPU usage over time') - .color(#1E90FF) ----------------------------------- - -<1> `.color()` changes the color of the data. Supported color types include standard color names, hexadecimal values, or a color schema for grouped data. In this example, `.color(gray)` represents the last hour, and `.color(#1E90FF)` represents the current hour. - -[role="screenshot"] -image::images/timelion-customize03.png[] -{nbsp} - -[float] -[[time-series-adjust-legend]] -==== Make adjustments to the legend - -Change the position and style of the legend: - -[source,text] ----------------------------------- -.es(offset=-1h, - index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('last hour') - .lines(fill=1,width=0.5) - .color(gray), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('current hour') - .title('CPU usage over time') - .color(#1E90FF) - .legend(columns=2, position=nw) <1> ----------------------------------- - -<1> `.legend()` sets the position and style of the legend. In this example, `.legend(columns=2, position=nw)` places the legend in the north west position of the visualization with two columns. - -[role="screenshot"] -image::images/timelion-customize04.png[] -{nbsp} - -[float] -[[mathematical-functions-intro]] -=== Create visualizations with mathematical functions - -To create a visualization for inbound and outbound network traffic, use mathematical functions. - -[float] -[[mathematical-functions-define-functions]] -==== Define the functions - -To start tracking the inbound and outbound network traffic, enter the following in the *Timelion Expression* field: - -[source,text] ----------------------------------- -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.in.bytes) ----------------------------------- - -[role="screenshot"] -image::images/timelion-math01.png[] -{nbsp} - -[float] -[[mathematical-functions-plot-change]] -==== Plot the rate of change - -Change how the data is displayed so that you can easily monitor the inbound traffic: - -[source,text] ----------------------------------- -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.in.bytes) - .derivative() <1> ----------------------------------- - -<1> `.derivative` plots the change in values over time. - -[role="screenshot"] -image::images/timelion-math02.png[] -{nbsp} - -Add a similar calculation for outbound traffic: - -[source,text] ----------------------------------- -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.in.bytes) - .derivative(), -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.out.bytes) - .derivative() - .multiply(-1) <1> ----------------------------------- - -<1> `.multiply()` multiplies the data series by a number, the result of a data series, or a list of data series. For this example, `.multiply(-1)` converts the outbound network traffic to a negative value since the outbound network traffic is leaving your machine. - -[role="screenshot"] -image::images/timelion-math03.png[] -{nbsp} - -[float] -[[mathematical-functions-convert-data]] -==== Change the data metric - -To make the visualization easier to analyze, change the data metric from bytes to megabytes: - -[source,text] ----------------------------------- -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.in.bytes) - .derivative() - .divide(1048576), -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.out.bytes) - .derivative() - .multiply(-1) - .divide(1048576) <1> ----------------------------------- - -<1> `.divide()` accepts the same input as `.multiply()`, then divides the data series by the defined divisor. - -[role="screenshot"] -image::images/timelion-math04.png[] -{nbsp} - -[float] -[[mathematical-functions-add-labels]] -==== Customize and format the visualization - -Customize and format the visualization using functions: - -[source,text] ----------------------------------- -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.in.bytes) - .derivative() - .divide(1048576) - .lines(fill=2, width=1) - .color(green) - .label("Inbound traffic") <1> - .title("Network traffic (MB/s)"), <2> -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.out.bytes) - .derivative() - .multiply(-1) - .divide(1048576) - .lines(fill=2, width=1) <3> - .color(blue) <4> - .label("Outbound traffic") - .legend(columns=2, position=nw) <5> ----------------------------------- - -<1> `.label()` adds custom labels to the visualization. -<2> `.title()` adds a title with a meaningful name. -<3> `.lines()` changes the appearance of the chart lines. In this example, `.lines(fill=2, width=1)` sets the fill level to `2`, and the border width to `1`. -<4> `.color()` changes the color of the data. Supported color types include standard color names, hexadecimal values, or a color schema for grouped data. In this example, `.color(green)` represents the inbound network traffic, and `.color(blue)` represents the outbound network traffic. -<5> `.legend()` sets the position and style of the legend. For this example, `legend(columns=2, position=nw)` places the legend in the north west position of the visualization with two columns. - -[role="screenshot"] -image::images/timelion-math05.png[] -{nbsp} - -[float] -[[timelion-conditional-intro]] -=== Create visualizations with conditional logic and tracking trends - -To easily detect outliers and discover patterns over time, modify time series data with conditional logic and create a trend with a moving average. - -With Timelion conditional logic, you can use the following operator values to compare your data: - -[horizontal] -`eq`:: equal -`ne`:: not equal -`lt`:: less than -`lte`:: less than or equal to -`gt`:: greater than -`gte`:: greater than or equal to - -[float] -[[conditional-define-functions]] -==== Define the functions - -To chart the maximum value of `system.memory.actual.used.bytes`, enter the following in the *Timelion Expression* field: - -[source,text] ----------------------------------- -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') ----------------------------------- - -[role="screenshot"] -image::images/timelion-conditional01.png[] -{nbsp} - -[float] -[[conditional-track-memory]] -==== Track used memory - -To track the amount of memory used, create two thresholds: - -[source,text] ----------------------------------- -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt, <1> - 11300000000, <2> - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null) - .label('warning') - .color('#FFCC11'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt, - 11375000000, - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null) - .label('severe') - .color('red') ----------------------------------- - -<1> Timelion conditional logic for the _greater than_ operator. In this example, the warning threshold is 11.3GB (`11300000000`), and the severe threshold is 11.375GB (`11375000000`). If the threshold values are too high or low for your machine, adjust the values accordingly. -<2> `if()` compares each point to a number. If the condition evaluates to `true`, adjust the styling. If the condition evaluates to `false`, use the default styling. - -[role="screenshot"] -image::images/timelion-conditional02.png[] -{nbsp} - -[float] -[[conditional-determine-trend]] -==== Determine the trend - -To determine the trend, create a new data series: - -[source,text] ----------------------------------- -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt,11300000000, - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null) - .label('warning') - .color('#FFCC11'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt,11375000000, - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null). - label('severe') - .color('red'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .mvavg(10) <1> ----------------------------------- - -<1> `mvavg()` calculates the moving average over a specified period of time. In this example, `.mvavg(10)` creates a moving average with a window of 10 data points. - -[role="screenshot"] -image::images/timelion-conditional03.png[] -{nbsp} - -[float] -[[conditional-format-visualization]] -==== Customize and format the visualization - -Customize and format the visualization using functions: - -[source,text] ----------------------------------- -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .label('max memory') <1> - .title('Memory consumption over time'), <2> -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt, - 11300000000, - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null) - .label('warning') - .color('#FFCC11') <3> - .lines(width=5), <4> -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt, - 11375000000, - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null) - .label('severe') - .color('red') - .lines(width=5), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .mvavg(10) - .label('mvavg') - .lines(width=2) - .color(#5E5E5E) - .legend(columns=4, position=nw) <5> ----------------------------------- - -<1> `.label()` adds custom labels to the visualization. -<2> `.title()` adds a title with a meaningful name. -<3> `.color()` changes the color of the data. Supported color types include standard color names, hexadecimal values, or a color schema for grouped data. -<4> `.lines()` changes the appearance of the chart lines. In this example, .lines(width=5) sets border width to `5`. -<5> `.legend()` sets the position and style of the legend. For this example, `(columns=4, position=nw)` places the legend in the north west position of the visualization with four columns. - -[role="screenshot"] -image::images/timelion-conditional04.png[] -{nbsp} - -For additional information on Timelion conditional capabilities, go to https://www.elastic.co/blog/timeseries-if-then-else-with-timelion[I have but one .condition()]. - -[float] -[[timelion-deprecation]] -=== Timelion App deprecation - -Deprecated since 7.0, the Timelion app will be removed in 8.0. If you have any Timelion worksheets, you must migrate them to a dashboard. - -NOTE: Only the Timelion app is deprecated. {kib} continues to support Timelion visualizations on dashboards, in Visualize, and in Canvas. - -[float] -[[timelion-app-to-vis]] -==== Create a dashboard from a Timelion worksheet - -To replace a Timelion worksheet with a dashboard, follow the same process for adding a visualization. -In addition, you must migrate the Timelion graphs to Visualize. - -. Open the menu, click **Dashboard**, then click **Create dashboard**. - -. On the dashboard, click **Create New**, then select the Timelion visualization. -+ -[role="screenshot"] -image::images/timelion-create-new-dashboard.png[] -+ -The only thing you need is the Timelion expression for each graph. - -. Open the Timelion app on a new tab, select the chart you want to copy, and copy its expression. -+ -[role="screenshot"] -image::images/timelion-copy-expression.png[] - -. Return to the other tab and paste the copied expression to the *Timelion Expression* field and click **Update**. -+ -[role="screenshot"] -image::images/timelion-vis-paste-expression.png[] - -. Save the new visualization, give it a name, and click **Save and Return**. -+ -Your Timelion visualization will appear on the dashboard. Repeat this for all your charts on each worksheet. -+ -[role="screenshot"] -image::images/timelion-dashboard.png[] diff --git a/docs/visualize/tsvb.asciidoc b/docs/visualize/tsvb.asciidoc deleted file mode 100644 index 9a1e81670b654..0000000000000 --- a/docs/visualize/tsvb.asciidoc +++ /dev/null @@ -1,138 +0,0 @@ -[[TSVB]] -== TSVB - -TSVB is a time series data visualizer that allows you to use the full power of the -Elasticsearch aggregation framework. With TSVB, you can combine an infinite -number of aggregations to display complex data. - -NOTE: In Elasticsearch version 7.3.0 and later, the time series data visualizer is now referred to as TSVB instead of Time Series Visual Builder. - -[float] -[[tsvb-visualization-types]] -=== Types of TSVB visualizations - -TSVB comes with these types of visualizations: - -Time Series:: A histogram visualization that supports area, line, bar, and steps along with multiple y-axis. - -[role="screenshot"] -image:images/tsvb-screenshot.png["Time series visualization"] - -Metric:: A metric that displays the latest number in a data series. - -[role="screenshot"] -image:images/tsvb-metric.png["Metric visualization"] - -Top N:: A horizontal bar chart where the y-axis is based on a series of metrics, and the x-axis is the latest value in the series. - -[role="screenshot"] -image:images/tsvb-top-n.png["Top N visualization"] - -Gauge:: A single value gauge visualization based on the latest value in a series. - -[role="screenshot"] -image:images/tsvb-gauge.png["Gauge visualization"] - -Markdown:: Edit the data using using Markdown text and Mustache template syntax. - -[role="screenshot"] -image:images/tsvb-markdown.png["Markdown visualization"] - -Table:: Display data from multiple time series by defining the field group to show in the rows, and the columns of data to display. - -[role="screenshot"] -image:images/tsvb-table.png["Table visualization"] - -[float] -[[create-tsvb-visualization]] -=== Create TSVB visualizations - -To create a TSVB visualization, choose the data series you want to display, then choose how you want to display the data. The options available are dependent on the visualization. - -[float] -[[tsvb-data-series-options]] -==== Configure the data series - -To create a single metric, add multiple data series with multiple aggregations. - -. Select the visualization type. - -. Specify the data series labels and colors. - -.. Select *Data*. -+ -If you are using the *Table* visualization, select *Columns*. - -.. In the *Label* field, enter a name for the data series, which is used on legends and titles. -+ -For series that are grouped by a term, you can specify a mustache variable of `{{key}}` to substitute the term. - -.. If supported by the visualization, click the swatch and choose a color for the data series. - -.. To add another data series, click *+*, then repeat the steps to specify the labels and colors. - -. Specify the data series metrics. - -.. Select *Metrics*. - -.. From the dropdown lists, choose your options. - -.. To add another metric, click *+*. -+ -When you add more than one metric, the last metric value is displayed, which is indicated by the eye icon. - -. To specify the format and display options, select *Options*. - -. To specify how to group or split the data, choose an option from the *Group by* drop down list. -+ -By default, the data series are grouped by everything. - -[float] -[[tsvb-panel-options]] -==== Configure the panel - -Change the data that you want to display and choose the style options for the panel. - -. Select *Panel options*. - -. Under *Data*, specify how much of the data that you want to display in the visualization. - -. Under *Style*, specify how you want the visualization to look. - -[float] -[[tsvb-add-annotations]] -==== Add annotations - -If you are using the Time Series visualization, add annotation data sources. - -. Select *Annotations*. - -. Click *Add data source*, then specify the options. - -[float] -[[tsvb-enter-markdown]] -==== Enter Markdown text - -Edit the source for the Markdown visualization. - -. Select *Markdown*. - -. In the editor, enter enter your Markdown text, then press Enter. - -. To insert the mustache template variable into the editor, click the variable name. -+ -The http://mustache.github.io/mustache.5.html[mustache syntax] uses the Handlebar.js processor, which is an extended version of the Mustache template language. - -[float] -[[tsvb-style-markdown]] -==== Style Markdown text - -Style your Markdown visualization using http://lesscss.org/features/[less syntax]. - -. Select *Markdown*. - -. Select *Panel options*. - -. Enter styling rules in *Custom CSS* section -+ -Less in TSVB does not support custom plugins or inline JavaScript. diff --git a/examples/alerting_example/tsconfig.json b/examples/alerting_example/tsconfig.json index fbcec9de439bd..09c130aca4642 100644 --- a/examples/alerting_example/tsconfig.json +++ b/examples/alerting_example/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target" }, diff --git a/examples/bfetch_explorer/tsconfig.json b/examples/bfetch_explorer/tsconfig.json index d508076b33199..798a9c222c5ab 100644 --- a/examples/bfetch_explorer/tsconfig.json +++ b/examples/bfetch_explorer/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/dashboard_embeddable_examples/tsconfig.json b/examples/dashboard_embeddable_examples/tsconfig.json index d508076b33199..798a9c222c5ab 100644 --- a/examples/dashboard_embeddable_examples/tsconfig.json +++ b/examples/dashboard_embeddable_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/developer_examples/tsconfig.json b/examples/developer_examples/tsconfig.json index d508076b33199..798a9c222c5ab 100644 --- a/examples/developer_examples/tsconfig.json +++ b/examples/developer_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/embeddable_examples/tsconfig.json b/examples/embeddable_examples/tsconfig.json index 7fa03739119b4..caeed2c1a434f 100644 --- a/examples/embeddable_examples/tsconfig.json +++ b/examples/embeddable_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/embeddable_explorer/tsconfig.json b/examples/embeddable_explorer/tsconfig.json index d508076b33199..798a9c222c5ab 100644 --- a/examples/embeddable_explorer/tsconfig.json +++ b/examples/embeddable_explorer/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/routing_example/tsconfig.json b/examples/routing_example/tsconfig.json index 9bbd9021b2e0a..761a5c4da65ba 100644 --- a/examples/routing_example/tsconfig.json +++ b/examples/routing_example/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/search_examples/tsconfig.json b/examples/search_examples/tsconfig.json index 8a3ced743d0fa..8bec69ca40ccc 100644 --- a/examples/search_examples/tsconfig.json +++ b/examples/search_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/state_containers_examples/tsconfig.json b/examples/state_containers_examples/tsconfig.json index 3f43072c2aade..007322e2d9525 100644 --- a/examples/state_containers_examples/tsconfig.json +++ b/examples/state_containers_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/ui_action_examples/tsconfig.json b/examples/ui_action_examples/tsconfig.json index d508076b33199..798a9c222c5ab 100644 --- a/examples/ui_action_examples/tsconfig.json +++ b/examples/ui_action_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/ui_actions_explorer/tsconfig.json b/examples/ui_actions_explorer/tsconfig.json index 199fbe1fcfa26..119209114a7bb 100644 --- a/examples/ui_actions_explorer/tsconfig.json +++ b/examples/ui_actions_explorer/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/url_generators_examples/tsconfig.json b/examples/url_generators_examples/tsconfig.json index 091130487791b..327b4642a8e7f 100644 --- a/examples/url_generators_examples/tsconfig.json +++ b/examples/url_generators_examples/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/examples/url_generators_explorer/tsconfig.json b/examples/url_generators_explorer/tsconfig.json index 091130487791b..327b4642a8e7f 100644 --- a/examples/url_generators_explorer/tsconfig.json +++ b/examples/url_generators_explorer/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true diff --git a/package.json b/package.json index 28f2025300f39..ff487510f7a32 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "kbn:watch": "node scripts/kibana --dev --logging.json=false", "build:types": "tsc --p tsconfig.types.json", "docs:acceptApiChanges": "node --max-old-space-size=6144 scripts/check_published_api_changes.js --accept", - "kbn:bootstrap": "node scripts/register_git_hook", + "kbn:bootstrap": "node scripts/build_ts_refs && node scripts/register_git_hook", "spec_to_console": "node scripts/spec_to_console", "backport-skip-ci": "backport --prDescription \"[skip-ci]\"", "storybook": "node scripts/storybook", @@ -92,6 +92,7 @@ "**/istanbul-instrumenter-loader/schema-utils": "1.0.0", "**/image-diff/gm/debug": "^2.6.9", "**/load-grunt-config/lodash": "^4.17.20", + "**/node-jose/node-forge": "^0.10.0", "**/react-dom": "^16.12.0", "**/react": "^16.12.0", "**/react-test-renderer": "^16.12.0", @@ -191,7 +192,7 @@ "moment-timezone": "^0.5.27", "mustache": "2.3.2", "node-fetch": "1.7.3", - "node-forge": "^0.9.1", + "node-forge": "^0.10.0", "opn": "^5.5.0", "oppsy": "^2.0.0", "p-map": "^4.0.0", @@ -305,7 +306,7 @@ "@types/moment-timezone": "^0.5.12", "@types/mustache": "^0.8.31", "@types/node": ">=10.17.17 <10.20.0", - "@types/node-forge": "^0.9.0", + "@types/node-forge": "^0.9.5", "@types/normalize-path": "^3.0.0", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", @@ -366,7 +367,6 @@ "dedent": "^0.7.0", "deepmerge": "^4.2.2", "delete-empty": "^2.0.0", - "elasticsearch-browser": "^16.7.0", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "enzyme-adapter-utils": "^1.13.0", diff --git a/packages/elastic-datemath/tsconfig.json b/packages/elastic-datemath/tsconfig.json index 3604f1004cf6c..cbfe1e8047433 100644 --- a/packages/elastic-datemath/tsconfig.json +++ b/packages/elastic-datemath/tsconfig.json @@ -1,6 +1,9 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/elastic-datemath" + }, "include": [ "index.d.ts" - ], + ] } diff --git a/packages/elastic-safer-lodash-set/package.json b/packages/elastic-safer-lodash-set/package.json index f0f425661f605..7602f2fa5924f 100644 --- a/packages/elastic-safer-lodash-set/package.json +++ b/packages/elastic-safer-lodash-set/package.json @@ -16,7 +16,7 @@ "scripts": { "lint": "dependency-check --no-dev package.json set.js setWith.js fp/*.js", "test": "npm run lint && tape test/*.js && npm run test:types", - "test:types": "./scripts/tsd.sh", + "test:types": "tsc --noEmit", "update": "./scripts/update.sh", "save_state": "./scripts/save_state.sh" }, @@ -42,8 +42,5 @@ "ignore": [ "/lodash/" ] - }, - "tsd": { - "directory": "test" } } diff --git a/packages/elastic-safer-lodash-set/scripts/tsd.sh b/packages/elastic-safer-lodash-set/scripts/tsd.sh deleted file mode 100755 index 4572367df415d..0000000000000 --- a/packages/elastic-safer-lodash-set/scripts/tsd.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -# Elasticsearch B.V licenses this file to you under the MIT License. -# See `packages/elastic-safer-lodash-set/LICENSE` for more information. - -# tsd will get confused if it finds a tsconfig.json file in the project -# directory and start to scan the entirety of Kibana. We don't want that. -mv tsconfig.json tsconfig.tmp - -clean_up () { - exit_code=$? - mv tsconfig.tmp tsconfig.json - exit $exit_code -} -trap clean_up EXIT - -./node_modules/.bin/tsd diff --git a/packages/elastic-safer-lodash-set/test/fp.test-d.ts b/packages/elastic-safer-lodash-set/test/fp.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/fp.test-d.ts rename to packages/elastic-safer-lodash-set/test/fp.ts diff --git a/packages/elastic-safer-lodash-set/test/fp_assoc.test-d.ts b/packages/elastic-safer-lodash-set/test/fp_assoc.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/fp_assoc.test-d.ts rename to packages/elastic-safer-lodash-set/test/fp_assoc.ts diff --git a/packages/elastic-safer-lodash-set/test/fp_assocPath.test-d.ts b/packages/elastic-safer-lodash-set/test/fp_assocPath.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/fp_assocPath.test-d.ts rename to packages/elastic-safer-lodash-set/test/fp_assocPath.ts diff --git a/packages/elastic-safer-lodash-set/test/fp_set.test-d.ts b/packages/elastic-safer-lodash-set/test/fp_set.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/fp_set.test-d.ts rename to packages/elastic-safer-lodash-set/test/fp_set.ts diff --git a/packages/elastic-safer-lodash-set/test/fp_setWith.test-d.ts b/packages/elastic-safer-lodash-set/test/fp_setWith.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/fp_setWith.test-d.ts rename to packages/elastic-safer-lodash-set/test/fp_setWith.ts diff --git a/packages/elastic-safer-lodash-set/test/index.test-d.ts b/packages/elastic-safer-lodash-set/test/index.ts similarity index 96% rename from packages/elastic-safer-lodash-set/test/index.test-d.ts rename to packages/elastic-safer-lodash-set/test/index.ts index ab29d7de5a03f..2090c1adcfce1 100644 --- a/packages/elastic-safer-lodash-set/test/index.test-d.ts +++ b/packages/elastic-safer-lodash-set/test/index.ts @@ -4,7 +4,7 @@ */ import { expectType } from 'tsd'; -import { set, setWith } from '../'; +import { set, setWith } from '..'; const someObj: object = {}; const anyValue: any = 'any value'; diff --git a/packages/elastic-safer-lodash-set/test/set.test-d.ts b/packages/elastic-safer-lodash-set/test/set.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/set.test-d.ts rename to packages/elastic-safer-lodash-set/test/set.ts diff --git a/packages/elastic-safer-lodash-set/test/setWith.test-d.ts b/packages/elastic-safer-lodash-set/test/setWith.ts similarity index 100% rename from packages/elastic-safer-lodash-set/test/setWith.test-d.ts rename to packages/elastic-safer-lodash-set/test/setWith.ts diff --git a/packages/elastic-safer-lodash-set/tsconfig.json b/packages/elastic-safer-lodash-set/tsconfig.json index bc1d1a3a7e413..6517e5c60ee01 100644 --- a/packages/elastic-safer-lodash-set/tsconfig.json +++ b/packages/elastic-safer-lodash-set/tsconfig.json @@ -1,9 +1,9 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/elastic-safer-lodash-set" + }, "include": [ - "**/*" + "**/*", ], - "exclude": [ - "**/*.test-d.ts" - ] } diff --git a/packages/kbn-analytics/scripts/build.js b/packages/kbn-analytics/scripts/build.js index 448d1ca9332f2..0e00a144d0b92 100644 --- a/packages/kbn-analytics/scripts/build.js +++ b/packages/kbn-analytics/scripts/build.js @@ -71,7 +71,6 @@ run( proc.run(padRight(10, 'tsc'), { cmd: 'tsc', args: [ - '--emitDeclarationOnly', ...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []), ...(flags['source-maps'] ? ['--declarationMap', 'true'] : []), ], diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index fdd9e8281fba8..861e0204a31a2 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -1,8 +1,9 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "declaration": true, - "declarationDir": "./target/types", + "emitDeclarationOnly": true, + "outDir": "./target/types", "stripInternal": true, "declarationMap": true, "types": [ @@ -11,7 +12,7 @@ ] }, "include": [ - "./src/**/*.ts" + "src/**/*" ], "exclude": [ "target" diff --git a/packages/kbn-config-schema/tsconfig.json b/packages/kbn-config-schema/tsconfig.json index f6c61268da17c..6a268f2e7c016 100644 --- a/packages/kbn-config-schema/tsconfig.json +++ b/packages/kbn-config-schema/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "declaration": true, "declarationDir": "./target/types", diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 0ec058eeb8a28..1c6c671d0b768 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "target", "target": "ES2019", diff --git a/packages/kbn-es-archiver/src/actions/save.ts b/packages/kbn-es-archiver/src/actions/save.ts index 2f87cabadee6c..84a0ce09936d0 100644 --- a/packages/kbn-es-archiver/src/actions/save.ts +++ b/packages/kbn-es-archiver/src/actions/save.ts @@ -39,6 +39,7 @@ export async function saveAction({ dataDir, log, raw, + query, }: { name: string; indices: string | string[]; @@ -46,6 +47,7 @@ export async function saveAction({ dataDir: string; log: ToolingLog; raw: boolean; + query?: Record; }) { const outputDir = resolve(dataDir, name); const stats = createStats(name, log); @@ -69,7 +71,7 @@ export async function saveAction({ // export all documents from matching indexes into data.json.gz createPromiseFromStreams([ createListStream(indices), - createGenerateDocRecordsStream(client, stats, progress), + createGenerateDocRecordsStream({ client, stats, progress, query }), ...createFormatArchiveStreams({ gzip: !raw }), createWriteStream(resolve(outputDir, `data.json${raw ? '' : '.gz'}`)), ] as [Readable, ...Writable[]]), diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts index 1745bd862b434..41abe83c148cd 100644 --- a/packages/kbn-es-archiver/src/cli.ts +++ b/packages/kbn-es-archiver/src/cli.ts @@ -122,8 +122,10 @@ export function runCli() { `, flags: { boolean: ['raw'], + string: ['query'], help: ` --raw don't gzip the archives + --query query object to limit the documents being archived, needs to be properly escaped JSON `, }, async run({ flags, esArchiver }) { @@ -140,7 +142,17 @@ export function runCli() { throw createFlagError('--raw does not take a value'); } - await esArchiver.save(name, indices, { raw }); + const query = flags.query; + let parsedQuery; + if (typeof query === 'string') { + try { + parsedQuery = JSON.parse(query); + } catch (err) { + throw createFlagError('--query should be valid JSON'); + } + } + + await esArchiver.save(name, indices, { raw, query: parsedQuery }); }, }) .command({ diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index e335652195b86..d61e7d2a422e8 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -62,7 +62,11 @@ export class EsArchiver { * @property {Boolean} options.raw - should the archive be raw (unzipped) or not * @return Promise */ - async save(name: string, indices: string | string[], { raw = false }: { raw?: boolean } = {}) { + async save( + name: string, + indices: string | string[], + { raw = false, query }: { raw?: boolean; query?: Record } = {} + ) { return await saveAction({ name, indices, @@ -70,6 +74,7 @@ export class EsArchiver { client: this.client, dataDir: this.dataDir, log: this.log, + query, }); } diff --git a/packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts index 2214f7ae9f2ea..3c5fc742a6e10 100644 --- a/packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/__tests__/generate_doc_records_stream.ts @@ -47,7 +47,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { const progress = new Progress(); await createPromiseFromStreams([ createListStream(['logstash-*']), - createGenerateDocRecordsStream(client, stats, progress), + createGenerateDocRecordsStream({ client, stats, progress }), ]); expect(progress.getTotal()).to.be(0); @@ -74,7 +74,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { const progress = new Progress(); await createPromiseFromStreams([ createListStream(['logstash-*']), - createGenerateDocRecordsStream(client, stats, progress), + createGenerateDocRecordsStream({ client, stats, progress }), ]); expect(progress.getTotal()).to.be(0); @@ -115,7 +115,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { const progress = new Progress(); const docRecords = await createPromiseFromStreams([ createListStream(['index1', 'index2']), - createGenerateDocRecordsStream(client, stats, progress), + createGenerateDocRecordsStream({ client, stats, progress }), createConcatStream([]), ]); diff --git a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts index e255a0abc36c5..87c166fe275cc 100644 --- a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts @@ -25,7 +25,17 @@ import { Progress } from '../progress'; const SCROLL_SIZE = 1000; const SCROLL_TIMEOUT = '1m'; -export function createGenerateDocRecordsStream(client: Client, stats: Stats, progress: Progress) { +export function createGenerateDocRecordsStream({ + client, + stats, + progress, + query, +}: { + client: Client; + stats: Stats; + progress: Progress; + query?: Record; +}) { return new Transform({ writableObjectMode: true, readableObjectMode: true, @@ -41,6 +51,9 @@ export function createGenerateDocRecordsStream(client: Client, stats: Stats, pro scroll: SCROLL_TIMEOUT, size: SCROLL_SIZE, _source: true, + body: { + query, + }, rest_total_hits_as_int: true, // not declared on SearchParams type } as SearchParams); remainingHits = resp.hits.total; diff --git a/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts index b4b98f8ae262c..07ee1420741c9 100644 --- a/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts @@ -37,6 +37,10 @@ export function createGenerateIndexRecordsStream(client: Client, stats: Stats) { '-*.settings.index.uuid', '-*.settings.index.version', '-*.settings.index.provided_name', + '-*.settings.index.frozen', + '-*.settings.index.search.throttled', + '-*.settings.index.query', + '-*.settings.index.routing', ], })) as Record; diff --git a/src/legacy/utils/streams/concat_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/concat_stream.test.js similarity index 100% rename from src/legacy/utils/streams/concat_stream.test.js rename to packages/kbn-es-archiver/src/lib/streams/concat_stream.test.js diff --git a/src/legacy/deprecation/deprecations/unused.js b/packages/kbn-es-archiver/src/lib/streams/concat_stream.ts similarity index 55% rename from src/legacy/deprecation/deprecations/unused.js rename to packages/kbn-es-archiver/src/lib/streams/concat_stream.ts index 4291063dc482b..03dd894067afc 100644 --- a/src/legacy/deprecation/deprecations/unused.js +++ b/packages/kbn-es-archiver/src/lib/streams/concat_stream.ts @@ -17,17 +17,25 @@ * under the License. */ -import { get, isUndefined, noop } from 'lodash'; -import { unset } from '../../utils'; +import { createReduceStream } from './reduce_stream'; -export function unused(oldKey) { - return (settings, log = noop) => { - const value = get(settings, oldKey); - if (isUndefined(value)) { - return; - } - - unset(settings, oldKey); - log(`${oldKey} is deprecated and is no longer used`); - }; +/** + * Creates a Transform stream that consumes all provided + * values and concatenates them using each values `concat` + * method. + * + * Concatenate strings: + * createListStream(['f', 'o', 'o']) + * .pipe(createConcatStream()) + * .on('data', console.log) + * // logs "foo" + * + * Concatenate values into an array: + * createListStream([1,2,3]) + * .pipe(createConcatStream([])) + * .on('data', console.log) + * // logs "[1,2,3]" + */ +export function createConcatStream(initial: any) { + return createReduceStream((acc, chunk) => acc.concat(chunk), initial); } diff --git a/src/legacy/utils/streams/concat_stream_providers.test.js b/packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.test.js similarity index 100% rename from src/legacy/utils/streams/concat_stream_providers.test.js rename to packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.test.js diff --git a/packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.ts b/packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.ts new file mode 100644 index 0000000000000..4794d76cc7f84 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/concat_stream_providers.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PassThrough, TransformOptions } from 'stream'; + +/** + * Write the data and errors from a list of stream providers + * to a single stream in order. Stream providers are only + * called right before they will be consumed, and only one + * provider will be active at a time. + */ +export function concatStreamProviders( + sourceProviders: Array<() => NodeJS.ReadableStream>, + options: TransformOptions = {} +) { + const destination = new PassThrough(options); + const queue = sourceProviders.slice(); + + (function pipeNext() { + const provider = queue.shift(); + + if (!provider) { + return; + } + + const source = provider(); + const isLast = !queue.length; + + // if there are more sources to pipe, hook + // into the source completion + if (!isLast) { + source.once('end', pipeNext); + } + + source + // proxy errors from the source to the destination + .once('error', (error) => destination.emit('error', error)) + // pipe the source to the destination but only proxy the + // end event if this is the last source + .pipe(destination, { end: isLast }); + })(); + + return destination; +} diff --git a/src/legacy/utils/streams/filter_stream.test.ts b/packages/kbn-es-archiver/src/lib/streams/filter_stream.test.ts similarity index 100% rename from src/legacy/utils/streams/filter_stream.test.ts rename to packages/kbn-es-archiver/src/lib/streams/filter_stream.test.ts diff --git a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js b/packages/kbn-es-archiver/src/lib/streams/filter_stream.ts similarity index 71% rename from src/legacy/core_plugins/kibana/server/ui_setting_defaults.js rename to packages/kbn-es-archiver/src/lib/streams/filter_stream.ts index 7de5fb581643a..738b9d5793d06 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/packages/kbn-es-archiver/src/lib/streams/filter_stream.ts @@ -17,7 +17,17 @@ * under the License. */ -export function getUiSettingDefaults() { - // wrapped in provider so that a new instance is given to each app/test - return {}; +import { Transform } from 'stream'; + +export function createFilterStream(fn: (obj: T) => boolean) { + return new Transform({ + objectMode: true, + async transform(obj, _, done) { + const canPushDownStream = fn(obj); + if (canPushDownStream) { + this.push(obj); + } + done(); + }, + }); } diff --git a/src/legacy/utils/streams/index.js b/packages/kbn-es-archiver/src/lib/streams/index.ts similarity index 100% rename from src/legacy/utils/streams/index.js rename to packages/kbn-es-archiver/src/lib/streams/index.ts diff --git a/src/legacy/utils/streams/intersperse_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/intersperse_stream.test.js similarity index 100% rename from src/legacy/utils/streams/intersperse_stream.test.js rename to packages/kbn-es-archiver/src/lib/streams/intersperse_stream.test.js diff --git a/packages/kbn-es-archiver/src/lib/streams/intersperse_stream.ts b/packages/kbn-es-archiver/src/lib/streams/intersperse_stream.ts new file mode 100644 index 0000000000000..eb2e3d3087d4a --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/intersperse_stream.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Transform } from 'stream'; + +/** + * Create a Transform stream that receives values in object mode, + * and intersperses a chunk between each object received. + * + * This is useful for writing lists: + * + * createListStream(['foo', 'bar']) + * .pipe(createIntersperseStream('\n')) + * .pipe(process.stdout) // outputs "foo\nbar" + * + * Combine with a concat stream to get "join" like functionality: + * + * await createPromiseFromStreams([ + * createListStream(['foo', 'bar']), + * createIntersperseStream(' '), + * createConcatStream() + * ]) // produces a single value "foo bar" + */ +export function createIntersperseStream(intersperseChunk: any) { + let first = true; + + return new Transform({ + writableObjectMode: true, + readableObjectMode: true, + transform(chunk, _, callback) { + try { + if (first) { + first = false; + } else { + this.push(intersperseChunk); + } + + this.push(chunk); + callback(undefined); + } catch (err) { + callback(err); + } + }, + }); +} diff --git a/src/legacy/utils/streams/list_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/list_stream.test.js similarity index 100% rename from src/legacy/utils/streams/list_stream.test.js rename to packages/kbn-es-archiver/src/lib/streams/list_stream.test.js diff --git a/src/legacy/deprecation/get_transform.js b/packages/kbn-es-archiver/src/lib/streams/list_stream.ts similarity index 63% rename from src/legacy/deprecation/get_transform.js rename to packages/kbn-es-archiver/src/lib/streams/list_stream.ts index bf286901af62c..c061b969b3c09 100644 --- a/src/legacy/deprecation/get_transform.js +++ b/packages/kbn-es-archiver/src/lib/streams/list_stream.ts @@ -17,14 +17,25 @@ * under the License. */ -import { noop } from 'lodash'; +import { Readable } from 'stream'; -import { createTransform } from './create_transform'; -import { rename, unused } from './deprecations'; +/** + * Create a Readable stream that provides the items + * from a list as objects to subscribers + */ +export function createListStream(items: any | any[] = []) { + const queue: any[] = [].concat(items); + + return new Readable({ + objectMode: true, + read(size) { + queue.splice(0, size).forEach((item) => { + this.push(item); + }); -export async function getTransform(spec) { - const deprecationsProvider = spec.getDeprecationsProvider() || noop; - if (!deprecationsProvider) return; - const transforms = (await deprecationsProvider({ rename, unused })) || []; - return createTransform(transforms); + if (!queue.length) { + this.push(null); + } + }, + }); } diff --git a/src/legacy/utils/streams/map_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/map_stream.test.js similarity index 100% rename from src/legacy/utils/streams/map_stream.test.js rename to packages/kbn-es-archiver/src/lib/streams/map_stream.test.js diff --git a/src/legacy/deprecation/create_transform.js b/packages/kbn-es-archiver/src/lib/streams/map_stream.ts similarity index 69% rename from src/legacy/deprecation/create_transform.js rename to packages/kbn-es-archiver/src/lib/streams/map_stream.ts index 72e8e153ed819..e88c512a38653 100644 --- a/src/legacy/deprecation/create_transform.js +++ b/packages/kbn-es-archiver/src/lib/streams/map_stream.ts @@ -17,17 +17,20 @@ * under the License. */ -import { deepCloneWithBuffers as clone } from '../utils'; -import { forEach, noop } from 'lodash'; +import { Transform } from 'stream'; -export function createTransform(deprecations) { - return (settings, log = noop) => { - const result = clone(settings); +export function createMapStream(fn: (chunk: any, i: number) => T | Promise) { + let i = 0; - forEach(deprecations, (deprecation) => { - deprecation(result, log); - }); - - return result; - }; + return new Transform({ + objectMode: true, + async transform(value, _, done) { + try { + this.push(await fn(value, i++)); + done(); + } catch (err) { + done(err); + } + }, + }); } diff --git a/src/legacy/utils/streams/promise_from_streams.test.js b/packages/kbn-es-archiver/src/lib/streams/promise_from_streams.test.js similarity index 100% rename from src/legacy/utils/streams/promise_from_streams.test.js rename to packages/kbn-es-archiver/src/lib/streams/promise_from_streams.test.js diff --git a/src/legacy/utils/streams/promise_from_streams.js b/packages/kbn-es-archiver/src/lib/streams/promise_from_streams.ts similarity index 85% rename from src/legacy/utils/streams/promise_from_streams.js rename to packages/kbn-es-archiver/src/lib/streams/promise_from_streams.ts index 05f6a08aa1a09..fefb18be14780 100644 --- a/src/legacy/utils/streams/promise_from_streams.js +++ b/packages/kbn-es-archiver/src/lib/streams/promise_from_streams.ts @@ -29,15 +29,15 @@ * * Errors emitted from any stream will cause * the promise to be rejected with that error. - * - * @param {Array} streams - * @return {Promise} */ import { pipeline, Writable } from 'stream'; +import { promisify } from 'util'; + +const asyncPipeline = promisify(pipeline); -export async function createPromiseFromStreams(streams) { - let finalChunk; +export async function createPromiseFromStreams(streams: any): Promise { + let finalChunk: any; const last = streams[streams.length - 1]; if (typeof last.read !== 'function' && streams.length === 1) { // For a nicer error than what stream.pipeline throws @@ -50,17 +50,15 @@ export async function createPromiseFromStreams(streams) { // Use object mode even when "last" stream isn't. This allows to // capture the last chunk as-is. objectMode: true, - write(chunk, enc, done) { + write(chunk, _, done) { finalChunk = chunk; done(); }, }) ); } - return new Promise((resolve, reject) => { - pipeline(...streams, (err) => { - if (err) return reject(err); - resolve(finalChunk); - }); - }); + + await asyncPipeline(...(streams as [any])); + + return finalChunk; } diff --git a/src/legacy/utils/streams/reduce_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/reduce_stream.test.js similarity index 100% rename from src/legacy/utils/streams/reduce_stream.test.js rename to packages/kbn-es-archiver/src/lib/streams/reduce_stream.test.js diff --git a/packages/kbn-es-archiver/src/lib/streams/reduce_stream.ts b/packages/kbn-es-archiver/src/lib/streams/reduce_stream.ts new file mode 100644 index 0000000000000..d9458e9a11c33 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/reduce_stream.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Transform } from 'stream'; + +/** + * Create a transform stream that consumes each chunk it receives + * and passes it to the reducer, which will return the new value + * for the stream. Once all chunks have been received the reduce + * stream provides the result of final call to the reducer to + * subscribers. + */ +export function createReduceStream( + reducer: (acc: any, chunk: any, env: string) => any, + initial: any +) { + let i = -1; + let value = initial; + + // if the reducer throws an error then the value is + // considered invalid and the stream will never provide + // it to subscribers. We will also stop calling the + // reducer for any new data that is provided to us + let failed = false; + + if (typeof reducer !== 'function') { + throw new TypeError('reducer must be a function'); + } + + return new Transform({ + readableObjectMode: true, + writableObjectMode: true, + async transform(chunk, enc, callback) { + try { + if (failed) { + return callback(); + } + + i += 1; + if (i === 0 && initial === undefined) { + value = chunk; + } else { + value = await reducer(value, chunk, enc); + } + + callback(); + } catch (err) { + failed = true; + callback(err); + } + }, + + flush(callback) { + if (!failed) { + this.push(value); + } + + callback(); + }, + }); +} diff --git a/src/legacy/utils/streams/replace_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/replace_stream.test.js similarity index 100% rename from src/legacy/utils/streams/replace_stream.test.js rename to packages/kbn-es-archiver/src/lib/streams/replace_stream.test.js diff --git a/packages/kbn-es-archiver/src/lib/streams/replace_stream.ts b/packages/kbn-es-archiver/src/lib/streams/replace_stream.ts new file mode 100644 index 0000000000000..fe2ba1fcdf31c --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/replace_stream.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Transform } from 'stream'; + +export function createReplaceStream(toReplace: string, replacement: string) { + if (typeof toReplace !== 'string') { + throw new TypeError('toReplace must be a string'); + } + + let buffer = Buffer.alloc(0); + return new Transform({ + objectMode: false, + async transform(value, _, done) { + try { + buffer = Buffer.concat([buffer, value], buffer.length + value.length); + + while (true) { + // try to find the next instance of `toReplace` in buffer + const index = buffer.indexOf(toReplace); + + // if there is no next instance, break + if (index === -1) { + break; + } + + // flush everything to the left of the next instance + // of `toReplace` + this.push(buffer.slice(0, index)); + + // then flush an instance of `replacement` + this.push(replacement); + + // and finally update the buffer to include everything + // to the right of `toReplace`, dropping to replace from the buffer + buffer = buffer.slice(index + toReplace.length); + } + + // until now we have only flushed data that is to the left + // of a discovered instance of `toReplace`. If `toReplace` is + // never found this would lead to us buffering the entire stream. + // + // Instead, we only keep enough buffer to complete a potentially + // partial instance of `toReplace` + if (buffer.length > toReplace.length) { + // the entire buffer except the last `toReplace.length` bytes + // so that if all but one byte from `toReplace` is in the buffer, + // and the next chunk delivers the necessary byte, the buffer will then + // contain a complete `toReplace` token. + this.push(buffer.slice(0, buffer.length - toReplace.length)); + buffer = buffer.slice(-toReplace.length); + } + + done(); + } catch (err) { + done(err); + } + }, + + flush(callback) { + if (buffer.length) { + this.push(buffer); + } + + callback(); + }, + }); +} diff --git a/src/legacy/utils/streams/split_stream.test.js b/packages/kbn-es-archiver/src/lib/streams/split_stream.test.js similarity index 100% rename from src/legacy/utils/streams/split_stream.test.js rename to packages/kbn-es-archiver/src/lib/streams/split_stream.test.js diff --git a/packages/kbn-es-archiver/src/lib/streams/split_stream.ts b/packages/kbn-es-archiver/src/lib/streams/split_stream.ts new file mode 100644 index 0000000000000..1c9b59449bd92 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/streams/split_stream.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Transform } from 'stream'; + +/** + * Creates a Transform stream that consumes a stream of Buffers + * and produces a stream of strings (in object mode) by splitting + * the received bytes using the splitChunk. + * + * Ways this is behaves like String#split: + * - instances of splitChunk are removed from the input + * - splitChunk can be on any size + * - if there are no bytes found after the last splitChunk + * a final empty chunk is emitted + * + * Ways this deviates from String#split: + * - splitChunk cannot be a regexp + * - an empty string or Buffer will not produce a stream of individual + * bytes like `string.split('')` would + */ +export function createSplitStream(splitChunk: string) { + let unsplitBuffer = Buffer.alloc(0); + + return new Transform({ + writableObjectMode: false, + readableObjectMode: true, + transform(chunk, _, callback) { + try { + let i; + let toSplit = Buffer.concat([unsplitBuffer, chunk]); + while ((i = toSplit.indexOf(splitChunk)) !== -1) { + const slice = toSplit.slice(0, i); + toSplit = toSplit.slice(i + splitChunk.length); + this.push(slice.toString('utf8')); + } + + unsplitBuffer = toSplit; + callback(undefined); + } catch (err) { + callback(err); + } + }, + + flush(callback) { + try { + this.push(unsplitBuffer.toString('utf8')); + + callback(undefined); + } catch (err) { + callback(err); + } + }, + }); +} diff --git a/packages/kbn-es-archiver/tsconfig.json b/packages/kbn-es-archiver/tsconfig.json index 6ffa64d91fba0..02209a29e5817 100644 --- a/packages/kbn-es-archiver/tsconfig.json +++ b/packages/kbn-es-archiver/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "declaration": true, diff --git a/packages/kbn-es/tsconfig.json b/packages/kbn-es/tsconfig.json index 6bb61453c99e7..9487a28232684 100644 --- a/packages/kbn-es/tsconfig.json +++ b/packages/kbn-es/tsconfig.json @@ -1,6 +1,9 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-es" + }, "include": [ - "src/**/*.ts" + "src/**/*" ] } diff --git a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js index d4e234e3a6a2e..60a03ae8a104e 100755 --- a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js +++ b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js @@ -27,11 +27,7 @@ exports.getWebpackConfig = function (kibanaPath) { mainFields: ['browser', 'main'], modules: ['node_modules', resolve(kibanaPath, 'node_modules')], alias: { - // Kibana defaults https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/legacy/ui/ui_bundler_env.js#L30-L36 - ui: resolve(kibanaPath, 'src/legacy/ui/public'), - // Dev defaults for test bundle https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/core_plugins/tests_bundle/index.js#L73-L78 - ng_mock$: resolve(kibanaPath, 'src/test_utils/public/ng_mock'), fixtures: resolve(kibanaPath, 'src/fixtures'), test_utils: resolve(kibanaPath, 'src/test_utils/public'), }, diff --git a/packages/kbn-expect/tsconfig.json b/packages/kbn-expect/tsconfig.json index a09ae2d7ae641..ae7e9ff090cc2 100644 --- a/packages/kbn-expect/tsconfig.json +++ b/packages/kbn-expect/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-expect" + }, "include": [ "expect.js.d.ts" ] diff --git a/packages/kbn-i18n/scripts/build.js b/packages/kbn-i18n/scripts/build.js index 62e1a35f00399..1d2b5031e37d7 100644 --- a/packages/kbn-i18n/scripts/build.js +++ b/packages/kbn-i18n/scripts/build.js @@ -71,7 +71,6 @@ run( proc.run(padRight(10, 'tsc'), { cmd: 'tsc', args: [ - '--emitDeclarationOnly', ...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []), ...(flags['source-maps'] ? ['--declarationMap', 'true'] : []), ], diff --git a/packages/kbn-i18n/tsconfig.json b/packages/kbn-i18n/tsconfig.json index d3dae3078c1d7..c6380f1cde969 100644 --- a/packages/kbn-i18n/tsconfig.json +++ b/packages/kbn-i18n/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "include": [ "src/**/*.ts", "src/**/*.tsx", @@ -11,7 +11,8 @@ ], "compilerOptions": { "declaration": true, - "declarationDir": "./target/types", + "emitDeclarationOnly": true, + "outDir": "./target/types", "types": [ "jest", "node" diff --git a/packages/kbn-interpreter/tsconfig.json b/packages/kbn-interpreter/tsconfig.json index 63376a7ca1ae8..3b81bbb118a55 100644 --- a/packages/kbn-interpreter/tsconfig.json +++ b/packages/kbn-interpreter/tsconfig.json @@ -1,4 +1,7 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-interpreter" + }, "include": ["index.d.ts", "src/**/*.d.ts"] } diff --git a/packages/kbn-monaco/tsconfig.json b/packages/kbn-monaco/tsconfig.json index 95acfd32b24dd..6d3f433c6a6d1 100644 --- a/packages/kbn-monaco/tsconfig.json +++ b/packages/kbn-monaco/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "declaration": true, diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 6b07384910abb..9f2c5654a8bd4 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -79,7 +79,6 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: // or which have require() statements that should be ignored because the file is // already bundled with all its necessary depedencies noParse: [ - /[\/\\]node_modules[\/\\]elasticsearch-browser[\/\\]/, /[\/\\]node_modules[\/\\]lodash[\/\\]index\.js$/, /[\/\\]node_modules[\/\\]vega[\/\\]build[\/\\]vega\.js$/, ], diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json index e2994f4d02414..20b06b5658cbc 100644 --- a/packages/kbn-optimizer/tsconfig.json +++ b/packages/kbn-optimizer/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-optimizer" + }, "include": [ "index.d.ts", "src/**/*" diff --git a/packages/kbn-plugin-generator/tsconfig.json b/packages/kbn-plugin-generator/tsconfig.json index fc88223dae4b2..c54ff041d7065 100644 --- a/packages/kbn-plugin-generator/tsconfig.json +++ b/packages/kbn-plugin-generator/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "target", "target": "ES2019", diff --git a/packages/kbn-plugin-helpers/tsconfig.json b/packages/kbn-plugin-helpers/tsconfig.json index e794b11b14afa..651bc79d6e707 100644 --- a/packages/kbn-plugin-helpers/tsconfig.json +++ b/packages/kbn-plugin-helpers/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "target", "declaration": true, diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index eb2d0d2581a34..9a3bb1c687032 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -27651,6 +27651,7 @@ var eos = function(stream, opts, callback) { var rs = stream._readableState; var readable = opts.readable || (opts.readable !== false && stream.readable); var writable = opts.writable || (opts.writable !== false && stream.writable); + var cancelled = false; var onlegacyfinish = function() { if (!stream.writable) onfinish(); @@ -27675,8 +27676,13 @@ var eos = function(stream, opts, callback) { }; var onclose = function() { - if (readable && !(rs && rs.ended)) return callback.call(stream, new Error('premature close')); - if (writable && !(ws && ws.ended)) return callback.call(stream, new Error('premature close')); + process.nextTick(onclosenexttick); + }; + + var onclosenexttick = function() { + if (cancelled) return; + if (readable && !(rs && (rs.ended && !rs.destroyed))) return callback.call(stream, new Error('premature close')); + if (writable && !(ws && (ws.ended && !ws.destroyed))) return callback.call(stream, new Error('premature close')); }; var onrequest = function() { @@ -27701,6 +27707,7 @@ var eos = function(stream, opts, callback) { stream.on('close', onclose); return function() { + cancelled = true; stream.removeListener('complete', onfinish); stream.removeListener('abort', onclose); stream.removeListener('request', onrequest); diff --git a/packages/kbn-pm/tsconfig.json b/packages/kbn-pm/tsconfig.json index c13a9243c50aa..175c4701f2e5b 100644 --- a/packages/kbn-pm/tsconfig.json +++ b/packages/kbn-pm/tsconfig.json @@ -1,12 +1,13 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "include": [ "./index.d.ts", "./src/**/*.ts", - "./dist/*.d.ts", + "./dist/*.d.ts" ], "exclude": [], "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-pm", "types": [ "jest", "node" diff --git a/packages/kbn-release-notes/src/cli.ts b/packages/kbn-release-notes/src/cli.ts index 44b4a7a0282d2..7dcfa38078391 100644 --- a/packages/kbn-release-notes/src/cli.ts +++ b/packages/kbn-release-notes/src/cli.ts @@ -25,8 +25,7 @@ import { run, createFlagError, createFailError, REPO_ROOT } from '@kbn/dev-utils import { FORMATS, SomeFormat } from './formats'; import { - iterRelevantPullRequests, - getPr, + PrApi, Version, ClassifiedPr, streamFromIterable, @@ -48,6 +47,7 @@ export function runReleaseNotesCli() { if (!token || typeof token !== 'string') { throw createFlagError('--token must be defined'); } + const prApi = new PrApi(log, token); const version = Version.fromFlag(flags.version); if (!version) { @@ -80,7 +80,7 @@ export function runReleaseNotesCli() { } const summary = new IrrelevantPrSummary(log); - const pr = await getPr(token, number); + const pr = await prApi.getPr(number); log.success( inspect( { @@ -101,7 +101,7 @@ export function runReleaseNotesCli() { const summary = new IrrelevantPrSummary(log); const prsToReport: ClassifiedPr[] = []; - const prIterable = iterRelevantPullRequests(token, version, log); + const prIterable = prApi.iterRelevantPullRequests(version); for await (const pr of prIterable) { if (!isPrRelevant(pr, version, includeVersions, summary)) { continue; diff --git a/packages/kbn-release-notes/src/lib/classify_pr.ts b/packages/kbn-release-notes/src/lib/classify_pr.ts index c567935ab7e48..2dfe6916235ee 100644 --- a/packages/kbn-release-notes/src/lib/classify_pr.ts +++ b/packages/kbn-release-notes/src/lib/classify_pr.ts @@ -27,7 +27,7 @@ import { ASCIIDOC_SECTIONS, UNKNOWN_ASCIIDOC_SECTION, } from '../release_notes_config'; -import { PullRequest } from './pull_request'; +import { PullRequest } from './pr_api'; export interface ClassifiedPr extends PullRequest { area: Area; diff --git a/packages/kbn-release-notes/src/lib/index.ts b/packages/kbn-release-notes/src/lib/index.ts index 00d8f49cf763f..8d27a26d96d0a 100644 --- a/packages/kbn-release-notes/src/lib/index.ts +++ b/packages/kbn-release-notes/src/lib/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export * from './pull_request'; +export * from './pr_api'; export * from './version'; export * from './is_pr_relevant'; export * from './streams'; diff --git a/packages/kbn-release-notes/src/lib/irrelevant_pr_summary.ts b/packages/kbn-release-notes/src/lib/irrelevant_pr_summary.ts index 1a458a04c7740..ba82ab8780465 100644 --- a/packages/kbn-release-notes/src/lib/irrelevant_pr_summary.ts +++ b/packages/kbn-release-notes/src/lib/irrelevant_pr_summary.ts @@ -19,7 +19,7 @@ import { ToolingLog } from '@kbn/dev-utils'; -import { PullRequest } from './pull_request'; +import { PullRequest } from './pr_api'; import { Version } from './version'; export class IrrelevantPrSummary { diff --git a/packages/kbn-release-notes/src/lib/is_pr_relevant.ts b/packages/kbn-release-notes/src/lib/is_pr_relevant.ts index af2ef9440dede..452a14e919ed4 100644 --- a/packages/kbn-release-notes/src/lib/is_pr_relevant.ts +++ b/packages/kbn-release-notes/src/lib/is_pr_relevant.ts @@ -18,7 +18,7 @@ */ import { Version } from './version'; -import { PullRequest } from './pull_request'; +import { PullRequest } from './pr_api'; import { IGNORE_LABELS } from '../release_notes_config'; import { IrrelevantPrSummary } from './irrelevant_pr_summary'; diff --git a/packages/kbn-release-notes/src/lib/pr_api.ts b/packages/kbn-release-notes/src/lib/pr_api.ts new file mode 100644 index 0000000000000..1f26aa7ad86c3 --- /dev/null +++ b/packages/kbn-release-notes/src/lib/pr_api.ts @@ -0,0 +1,231 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { inspect } from 'util'; + +import Axios from 'axios'; +import gql from 'graphql-tag'; +import * as GraphqlPrinter from 'graphql/language/printer'; +import { DocumentNode } from 'graphql/language/ast'; +import makeTerminalLink from 'terminal-link'; +import { ToolingLog, isAxiosResponseError } from '@kbn/dev-utils'; + +import { Version } from './version'; +import { getFixReferences } from './get_fix_references'; +import { getNoteFromDescription } from './get_note_from_description'; + +const PrNodeFragment = gql` + fragment PrNode on PullRequest { + number + url + title + bodyText + bodyHTML + mergedAt + baseRefName + state + author { + login + ... on User { + name + } + } + labels(first: 100) { + nodes { + name + } + } + } +`; + +export interface PullRequest { + number: number; + url: string; + title: string; + targetBranch: string; + mergedAt: string; + state: string; + labels: string[]; + fixes: string[]; + user: { + name: string; + login: string; + }; + versions: Version[]; + terminalLink: string; + note?: string; +} + +export class PrApi { + constructor(private readonly log: ToolingLog, private readonly token: string) {} + + async getPr(number: number) { + const resp = await this.gqlRequest( + gql` + query($number: Int!) { + repository(owner: "elastic", name: "kibana") { + pullRequest(number: $number) { + ...PrNode + } + } + } + ${PrNodeFragment} + `, + { + number, + } + ); + + const node = resp.data?.repository?.pullRequest; + if (!node) { + throw new Error(`unexpected github response, unable to fetch PR: ${inspect(resp)}`); + } + + return this.parsePullRequestNode(node); + } + + /** + * Iterate all of the PRs which have the `version` label + */ + async *iterRelevantPullRequests(version: Version) { + let nextCursor: string | undefined; + let hasNextPage = true; + + while (hasNextPage) { + const resp = await this.gqlRequest( + gql` + query($cursor: String, $labels: [String!]) { + repository(owner: "elastic", name: "kibana") { + pullRequests(first: 100, after: $cursor, labels: $labels, states: MERGED) { + pageInfo { + hasNextPage + endCursor + } + nodes { + ...PrNode + } + } + } + } + ${PrNodeFragment} + `, + { + cursor: nextCursor, + labels: [version.label], + } + ); + + const pullRequests = resp.data?.repository?.pullRequests; + if (!pullRequests) { + throw new Error(`unexpected github response, unable to fetch PRs: ${inspect(resp)}`); + } + + hasNextPage = pullRequests.pageInfo?.hasNextPage; + nextCursor = pullRequests.pageInfo?.endCursor; + + if (hasNextPage === undefined || (hasNextPage && !nextCursor)) { + throw new Error( + `github response does not include valid pagination information: ${inspect(resp)}` + ); + } + + for (const node of pullRequests.nodes) { + yield this.parsePullRequestNode(node); + } + } + } + + /** + * Convert the Github API response into the structure used by this tool + * + * @param node A GraphQL response from Github using the PrNode fragment + */ + private parsePullRequestNode(node: any): PullRequest { + const terminalLink = makeTerminalLink(`#${node.number}`, node.url); + + const labels: string[] = node.labels.nodes.map((l: { name: string }) => l.name); + + return { + number: node.number, + url: node.url, + terminalLink, + title: node.title, + targetBranch: node.baseRefName, + state: node.state, + mergedAt: node.mergedAt, + labels, + fixes: getFixReferences(node.bodyText), + user: { + login: node.author?.login || 'deleted user', + name: node.author?.name, + }, + versions: labels + .map((l) => Version.fromLabel(l)) + .filter((v): v is Version => v instanceof Version), + note: getNoteFromDescription(node.bodyHTML), + }; + } + + /** + * Send a single request to the Github v4 GraphQL API + */ + private async gqlRequest(query: DocumentNode, variables: Record = {}) { + let attempt = 0; + + while (true) { + attempt += 1; + + try { + const resp = await Axios.request({ + url: 'https://api.github.com/graphql', + method: 'POST', + headers: { + 'user-agent': '@kbn/release-notes', + authorization: `bearer ${this.token}`, + }, + data: { + query: GraphqlPrinter.print(query), + variables, + }, + }); + + return resp.data; + } catch (error) { + if (!isAxiosResponseError(error) || error.response.status < 500) { + // rethrow error unless it is a 500+ response from github + throw error; + } + + const { status, data } = error.response; + const resp = inspect(data); + + if (attempt === 5) { + throw new Error( + `${status} response from Github, attempted request ${attempt} times: [${resp}]` + ); + } + + const delay = attempt * 2000; + this.log.debug(`Github responded with ${status}, retrying in ${delay} ms: [${resp}]`); + await new Promise((resolve) => setTimeout(resolve, delay)); + continue; + } + } + } +} diff --git a/packages/kbn-release-notes/src/lib/pull_request.ts b/packages/kbn-release-notes/src/lib/pull_request.ts deleted file mode 100644 index e61e496642062..0000000000000 --- a/packages/kbn-release-notes/src/lib/pull_request.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { inspect } from 'util'; - -import Axios from 'axios'; -import gql from 'graphql-tag'; -import * as GraphqlPrinter from 'graphql/language/printer'; -import { DocumentNode } from 'graphql/language/ast'; -import makeTerminalLink from 'terminal-link'; -import { ToolingLog } from '@kbn/dev-utils'; - -import { Version } from './version'; -import { getFixReferences } from './get_fix_references'; -import { getNoteFromDescription } from './get_note_from_description'; - -const PrNodeFragment = gql` - fragment PrNode on PullRequest { - number - url - title - bodyText - bodyHTML - mergedAt - baseRefName - state - author { - login - ... on User { - name - } - } - labels(first: 100) { - nodes { - name - } - } - } -`; - -export interface PullRequest { - number: number; - url: string; - title: string; - targetBranch: string; - mergedAt: string; - state: string; - labels: string[]; - fixes: string[]; - user: { - name: string; - login: string; - }; - versions: Version[]; - terminalLink: string; - note?: string; -} - -/** - * Send a single request to the Github v4 GraphQL API - */ -async function gqlRequest( - token: string, - query: DocumentNode, - variables: Record = {} -) { - const resp = await Axios.request({ - url: 'https://api.github.com/graphql', - method: 'POST', - headers: { - 'user-agent': '@kbn/release-notes', - authorization: `bearer ${token}`, - }, - data: { - query: GraphqlPrinter.print(query), - variables, - }, - }); - - return resp.data; -} - -/** - * Convert the Github API response into the structure used by this tool - * - * @param node A GraphQL response from Github using the PrNode fragment - */ -function parsePullRequestNode(node: any): PullRequest { - const terminalLink = makeTerminalLink(`#${node.number}`, node.url); - - const labels: string[] = node.labels.nodes.map((l: { name: string }) => l.name); - - return { - number: node.number, - url: node.url, - terminalLink, - title: node.title, - targetBranch: node.baseRefName, - state: node.state, - mergedAt: node.mergedAt, - labels, - fixes: getFixReferences(node.bodyText), - user: { - login: node.author?.login || 'deleted user', - name: node.author?.name, - }, - versions: labels - .map((l) => Version.fromLabel(l)) - .filter((v): v is Version => v instanceof Version), - note: getNoteFromDescription(node.bodyHTML), - }; -} - -/** - * Iterate all of the PRs which have the `version` label - */ -export async function* iterRelevantPullRequests(token: string, version: Version, log: ToolingLog) { - let nextCursor: string | undefined; - let hasNextPage = true; - - while (hasNextPage) { - const resp = await gqlRequest( - token, - gql` - query($cursor: String, $labels: [String!]) { - repository(owner: "elastic", name: "kibana") { - pullRequests(first: 100, after: $cursor, labels: $labels, states: MERGED) { - pageInfo { - hasNextPage - endCursor - } - nodes { - ...PrNode - } - } - } - } - ${PrNodeFragment} - `, - { - cursor: nextCursor, - labels: [version.label], - } - ); - - const pullRequests = resp.data?.repository?.pullRequests; - if (!pullRequests) { - throw new Error(`unexpected github response, unable to fetch PRs: ${inspect(resp)}`); - } - - hasNextPage = pullRequests.pageInfo?.hasNextPage; - nextCursor = pullRequests.pageInfo?.endCursor; - - if (hasNextPage === undefined || (hasNextPage && !nextCursor)) { - throw new Error( - `github response does not include valid pagination information: ${inspect(resp)}` - ); - } - - for (const node of pullRequests.nodes) { - yield parsePullRequestNode(node); - } - } -} - -export async function getPr(token: string, number: number) { - const resp = await gqlRequest( - token, - gql` - query($number: Int!) { - repository(owner: "elastic", name: "kibana") { - pullRequest(number: $number) { - ...PrNode - } - } - } - ${PrNodeFragment} - `, - { - number, - } - ); - - const node = resp.data?.repository?.pullRequest; - if (!node) { - throw new Error(`unexpected github response, unable to fetch PR: ${inspect(resp)}`); - } - - return parsePullRequestNode(node); -} diff --git a/packages/kbn-release-notes/tsconfig.json b/packages/kbn-release-notes/tsconfig.json index 6ffa64d91fba0..02209a29e5817 100644 --- a/packages/kbn-release-notes/tsconfig.json +++ b/packages/kbn-release-notes/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "declaration": true, diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 13ce8ef2bad60..98512053a5c92 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-telemetry-tools" + }, "include": [ "src/**/*", ] diff --git a/packages/kbn-test-subj-selector/tsconfig.json b/packages/kbn-test-subj-selector/tsconfig.json index 3604f1004cf6c..a1e1c1af372c6 100644 --- a/packages/kbn-test-subj-selector/tsconfig.json +++ b/packages/kbn-test-subj-selector/tsconfig.json @@ -1,6 +1,9 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-test-subj-selector" + }, "include": [ "index.d.ts" - ], + ] } diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts index 96fd525efa3ec..2e40aeec4f43d 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts @@ -21,7 +21,6 @@ import { ToolingLog } from '@kbn/dev-utils'; import { defaultsDeep } from 'lodash'; import { Config } from './config'; -import { transformDeprecations } from './transform_deprecations'; const cache = new WeakMap(); @@ -52,8 +51,7 @@ async function getSettingsFromFile(log: ToolingLog, path: string, settingOverrid await cache.get(configProvider)! ); - const logDeprecation = (error: string | Error) => log.error(error); - return transformDeprecations(settingsWithDefaults, logDeprecation); + return settingsWithDefaults; } export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any = {}) { diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/transform_deprecations.ts b/packages/kbn-test/src/functional_test_runner/lib/config/transform_deprecations.ts deleted file mode 100644 index 08dfc4a4f61f4..0000000000000 --- a/packages/kbn-test/src/functional_test_runner/lib/config/transform_deprecations.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// @ts-ignore -import { createTransform, Deprecations } from '../../../../../../src/legacy/deprecation'; - -type DeprecationTransformer = ( - settings: object, - log: (msg: string) => void -) => { - [key: string]: any; -}; - -export const transformDeprecations: DeprecationTransformer = createTransform([ - Deprecations.unused('servers.webdriver'), -]); diff --git a/packages/kbn-test/tsconfig.json b/packages/kbn-test/tsconfig.json index fdb53de52687b..fec35e45b2a15 100644 --- a/packages/kbn-test/tsconfig.json +++ b/packages/kbn-test/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-test" + }, "include": [ "types/**/*", "src/**/*", diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index 0f981f3d07610..365b84b83bd5f 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -54,6 +54,3 @@ export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme import * as Theme from './theme.ts'; export { Theme }; - -// massive deps that we should really get rid of or reduce in size substantially -export const ElasticsearchBrowser = require('elasticsearch-browser/elasticsearch.js'); diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 40e89f199b6a1..84ca3435e02bc 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -62,12 +62,5 @@ exports.externals = { '@elastic/eui/dist/eui_charts_theme': '__kbnSharedDeps__.ElasticEuiChartsTheme', '@elastic/eui/dist/eui_theme_light.json': '__kbnSharedDeps__.Theme.euiLightVars', '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.Theme.euiDarkVars', - - /** - * massive deps that we should really get rid of or reduce in size substantially - */ - elasticsearch: '__kbnSharedDeps__.ElasticsearchBrowser', - 'elasticsearch-browser': '__kbnSharedDeps__.ElasticsearchBrowser', - 'elasticsearch-browser/elasticsearch': '__kbnSharedDeps__.ElasticsearchBrowser', }; exports.publicPathLoader = require.resolve('./public_path_loader'); diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 0067228f1c1f3..4b2e88d155245 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -19,7 +19,6 @@ "compression-webpack-plugin": "^4.0.0", "core-js": "^3.6.4", "custom-event-polyfill": "^0.3.0", - "elasticsearch-browser": "^16.7.0", "jquery": "^3.5.0", "mini-css-extract-plugin": "0.8.0", "moment": "^2.24.0", diff --git a/packages/kbn-ui-shared-deps/tsconfig.json b/packages/kbn-ui-shared-deps/tsconfig.json index cef9a442d17bc..88699027f85de 100644 --- a/packages/kbn-ui-shared-deps/tsconfig.json +++ b/packages/kbn-ui-shared-deps/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-ui-shared-deps" + }, "include": [ "index.d.ts", "theme.ts" diff --git a/packages/kbn-utility-types/tsconfig.json b/packages/kbn-utility-types/tsconfig.json index 202df37faf561..03cace5b9cb2c 100644 --- a/packages/kbn-utility-types/tsconfig.json +++ b/packages/kbn-utility-types/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "declaration": true, "declarationDir": "./target", diff --git a/rfcs/text/0010_service_status.md b/rfcs/text/0010_service_status.md index 76195c4f1ab89..ded594930a367 100644 --- a/rfcs/text/0010_service_status.md +++ b/rfcs/text/0010_service_status.md @@ -137,7 +137,7 @@ interface StatusSetup { * Current status for all dependencies of the current plugin. * Each key of the `Record` is a plugin id. */ - dependencies$: Observable>; + plugins$: Observable>; /** * The status of this plugin as derived from its dependencies. diff --git a/packages/kbn-es-archiver/src/lib/streams.ts b/scripts/build_ts_refs.js similarity index 89% rename from packages/kbn-es-archiver/src/lib/streams.ts rename to scripts/build_ts_refs.js index a90afbe0c4d25..29fd66bab4ca9 100644 --- a/packages/kbn-es-archiver/src/lib/streams.ts +++ b/scripts/build_ts_refs.js @@ -16,5 +16,5 @@ * specific language governing permissions and limitations * under the License. */ - -export * from '../../../../src/legacy/utils/streams'; +require('../src/setup_node_env'); +require('../src/dev/typescript/build_refs').runBuildRefs(); diff --git a/src/cli_keystore/add.js b/src/cli_keystore/add.js index 44737e387c2d2..462259ec942dd 100644 --- a/src/cli_keystore/add.js +++ b/src/cli_keystore/add.js @@ -19,7 +19,7 @@ import { Logger } from '../cli_plugin/lib/logger'; import { confirm, question } from '../legacy/server/utils'; -import { createPromiseFromStreams, createConcatStream } from '../legacy/utils'; +import { createPromiseFromStreams, createConcatStream } from '../core/server/utils'; /** * @param {Keystore} keystore diff --git a/src/core/TESTING.md b/src/core/TESTING.md index a62922d9b5d64..a0fd0a6ffc255 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -330,7 +330,7 @@ Cons: To have access to Kibana TestUtils, you should create `integration_tests` folder and import `test_utils` within a test file: ```typescript // src/plugins/my_plugin/server/integration_tests/formatter.test.ts -import * as kbnTestServer from 'src/test_utils/kbn_server'; +import * as kbnTestServer from 'src/core/test_helpers/kbn_server'; describe('myPlugin', () => { describe('GET /myPlugin/formatter', () => { diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index 2bdf56ee34211..5609e7eb5d17d 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -28,7 +28,6 @@ import { ApplicationStart, InternalApplicationSetup, PublicAppInfo, - PublicLegacyAppInfo, } from './types'; import { ApplicationServiceContract } from './test_types'; @@ -40,7 +39,6 @@ const createSetupContractMock = (): jest.Mocked => ({ const createInternalSetupContractMock = (): jest.Mocked => ({ register: jest.fn(), - registerLegacyApp: jest.fn(), registerAppUpdater: jest.fn(), registerMountContext: jest.fn(), }); @@ -49,7 +47,7 @@ const createStartContractMock = (): jest.Mocked => { const currentAppId$ = new Subject(); return { - applications$: new BehaviorSubject>(new Map()), + applications$: new BehaviorSubject>(new Map()), currentAppId$: currentAppId$.asObservable(), capabilities: capabilitiesServiceMock.createStartContract().capabilities, navigateToApp: jest.fn(), @@ -85,7 +83,7 @@ const createInternalStartContractMock = (): jest.Mocked(); return { - applications$: new BehaviorSubject>(new Map()), + applications$: new BehaviorSubject>(new Map()), capabilities: capabilitiesServiceMock.createStartContract().capabilities, currentAppId$: currentAppId$.asObservable(), currentActionMenu$: new BehaviorSubject(undefined), diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index d0c2ac111eb1f..afcebc06506c2 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -28,21 +28,12 @@ import { BehaviorSubject, Subject } from 'rxjs'; import { bufferCount, take, takeUntil } from 'rxjs/operators'; import { shallow, mount } from 'enzyme'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { MockLifecycle } from './test_types'; import { ApplicationService } from './application_service'; -import { - App, - PublicAppInfo, - AppNavLinkStatus, - AppStatus, - AppUpdater, - LegacyApp, - PublicLegacyAppInfo, -} from './types'; +import { App, PublicAppInfo, AppNavLinkStatus, AppStatus, AppUpdater } from './types'; import { act } from 'react-dom/test-utils'; const createApp = (props: Partial): App => { @@ -54,15 +45,6 @@ const createApp = (props: Partial): App => { }; }; -const createLegacyApp = (props: Partial): LegacyApp => { - return { - id: 'some-id', - title: 'some-title', - appUrl: '/my-url', - ...props, - }; -}; - let setupDeps: MockLifecycle<'setup'>; let startDeps: MockLifecycle<'start'>; let service: ApplicationService; @@ -73,10 +55,8 @@ describe('#setup()', () => { setupDeps = { http, context: contextServiceMock.createSetupContract(), - injectedMetadata: injectedMetadataServiceMock.createSetupContract(), redirectTo: jest.fn(), }; - setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false); startDeps = { http, overlays: overlayServiceMock.createStartContract() }; service = new ApplicationService(); }); @@ -116,7 +96,6 @@ describe('#setup()', () => { expect(applications.get('app1')).toEqual( expect.objectContaining({ id: 'app1', - legacy: false, navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, }) @@ -124,7 +103,6 @@ describe('#setup()', () => { expect(applications.get('app2')).toEqual( expect.objectContaining({ id: 'app2', - legacy: false, navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, }) @@ -141,7 +119,6 @@ describe('#setup()', () => { expect(applications.get('app1')).toEqual( expect.objectContaining({ id: 'app1', - legacy: false, navLinkStatus: AppNavLinkStatus.hidden, status: AppStatus.inaccessible, defaultPath: 'foo/bar', @@ -151,7 +128,6 @@ describe('#setup()', () => { expect(applications.get('app2')).toEqual( expect.objectContaining({ id: 'app2', - legacy: false, navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, }) @@ -159,7 +135,7 @@ describe('#setup()', () => { }); it('throws an error if an App with the same appRoute is registered', () => { - const { register, registerLegacyApp } = service.setup(setupDeps); + const { register } = service.setup(setupDeps); register(Symbol(), createApp({ id: 'app1' })); @@ -168,7 +144,6 @@ describe('#setup()', () => { ).toThrowErrorMatchingInlineSnapshot( `"An application is already registered with the appRoute \\"/app/app1\\""` ); - expect(() => registerLegacyApp(createLegacyApp({ id: 'app1' }))).toThrow(); register(Symbol(), createApp({ id: 'app-next', appRoute: '/app/app3' })); @@ -177,7 +152,6 @@ describe('#setup()', () => { ).toThrowErrorMatchingInlineSnapshot( `"An application is already registered with the appRoute \\"/app/app3\\""` ); - expect(() => registerLegacyApp(createLegacyApp({ id: 'app3' }))).not.toThrow(); }); it('throws an error if an App starts with the HTTP base path', () => { @@ -195,41 +169,6 @@ describe('#setup()', () => { }); }); - describe('registerLegacyApp', () => { - it('throws an error if two apps with the same id are registered', () => { - const { registerLegacyApp } = service.setup(setupDeps); - - registerLegacyApp(createLegacyApp({ id: 'app2' })); - expect(() => - registerLegacyApp(createLegacyApp({ id: 'app2' })) - ).toThrowErrorMatchingInlineSnapshot( - `"An application is already registered with the id \\"app2\\""` - ); - }); - - it('throws error if additional apps are registered after setup', async () => { - const { registerLegacyApp } = service.setup(setupDeps); - - await service.start(startDeps); - expect(() => - registerLegacyApp(createLegacyApp({ id: 'app2' })) - ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`); - }); - - it('throws an error if a LegacyApp with the same appRoute is registered', () => { - const { register, registerLegacyApp } = service.setup(setupDeps); - - registerLegacyApp(createLegacyApp({ id: 'app1' })); - - expect(() => - register(Symbol(), createApp({ id: 'app2', appRoute: '/app/app1' })) - ).toThrowErrorMatchingInlineSnapshot( - `"An application is already registered with the appRoute \\"/app/app1\\""` - ); - expect(() => registerLegacyApp(createLegacyApp({ id: 'app1:other' }))).not.toThrow(); - }); - }); - describe('registerAppUpdater', () => { it('updates status fields', async () => { const setup = service.setup(setupDeps); @@ -258,7 +197,6 @@ describe('#setup()', () => { expect(applications.get('app1')).toEqual( expect.objectContaining({ id: 'app1', - legacy: false, navLinkStatus: AppNavLinkStatus.disabled, status: AppStatus.inaccessible, tooltip: 'App inaccessible due to reason', @@ -267,7 +205,6 @@ describe('#setup()', () => { expect(applications.get('app2')).toEqual( expect.objectContaining({ id: 'app2', - legacy: false, navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, tooltip: 'App accessible', @@ -307,7 +244,6 @@ describe('#setup()', () => { expect(applications.get('app1')).toEqual( expect.objectContaining({ id: 'app1', - legacy: false, navLinkStatus: AppNavLinkStatus.disabled, status: AppStatus.inaccessible, tooltip: 'App inaccessible due to reason', @@ -316,7 +252,6 @@ describe('#setup()', () => { expect(applications.get('app2')).toEqual( expect.objectContaining({ id: 'app2', - legacy: false, status: AppStatus.inaccessible, navLinkStatus: AppNavLinkStatus.hidden, }) @@ -352,7 +287,6 @@ describe('#setup()', () => { expect(applications.get('app1')).toEqual( expect.objectContaining({ id: 'app1', - legacy: false, navLinkStatus: AppNavLinkStatus.disabled, status: AppStatus.inaccessible, }) @@ -374,10 +308,7 @@ describe('#setup()', () => { setup.registerAppUpdater(statusUpdater); const start = await service.start(startDeps); - let latestValue: ReadonlyMap = new Map< - string, - PublicAppInfo | PublicLegacyAppInfo - >(); + let latestValue: ReadonlyMap = new Map(); start.applications$.subscribe((apps) => { latestValue = apps; }); @@ -385,7 +316,6 @@ describe('#setup()', () => { expect(latestValue.get('app1')).toEqual( expect.objectContaining({ id: 'app1', - legacy: false, status: AppStatus.inaccessible, navLinkStatus: AppNavLinkStatus.disabled, }) @@ -401,43 +331,12 @@ describe('#setup()', () => { expect(latestValue.get('app1')).toEqual( expect.objectContaining({ id: 'app1', - legacy: false, status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.hidden, }) ); }); - it('also updates legacy apps', async () => { - const setup = service.setup(setupDeps); - - setup.registerLegacyApp(createLegacyApp({ id: 'app1' })); - - setup.registerAppUpdater( - new BehaviorSubject((app) => { - return { - status: AppStatus.inaccessible, - navLinkStatus: AppNavLinkStatus.hidden, - tooltip: 'App inaccessible due to reason', - }; - }) - ); - - const start = await service.start(startDeps); - const applications = await start.applications$.pipe(take(1)).toPromise(); - - expect(applications.size).toEqual(1); - expect(applications.get('app1')).toEqual( - expect.objectContaining({ - id: 'app1', - legacy: true, - status: AppStatus.inaccessible, - navLinkStatus: AppNavLinkStatus.hidden, - tooltip: 'App inaccessible due to reason', - }) - ); - }); - it('allows to update the basePath', async () => { const setup = service.setup(setupDeps); @@ -486,10 +385,8 @@ describe('#start()', () => { setupDeps = { http, context: contextServiceMock.createSetupContract(), - injectedMetadata: injectedMetadataServiceMock.createSetupContract(), redirectTo: jest.fn(), }; - setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false); startDeps = { http, overlays: overlayServiceMock.createStartContract() }; service = new ApplicationService(); }); @@ -507,11 +404,10 @@ describe('#start()', () => { }); it('exposes available apps', async () => { - setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true); - const { register, registerLegacyApp } = service.setup(setupDeps); + const { register } = service.setup(setupDeps); register(Symbol(), createApp({ id: 'app1' })); - registerLegacyApp(createLegacyApp({ id: 'app2' })); + register(Symbol(), createApp({ id: 'app2' })); const { applications$ } = await service.start(startDeps); const availableApps = await applications$.pipe(take(1)).toPromise(); @@ -522,16 +418,14 @@ describe('#start()', () => { expect.objectContaining({ appRoute: '/app/app1', id: 'app1', - legacy: false, navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, }) ); expect(availableApps.get('app2')).toEqual( expect.objectContaining({ - appUrl: '/my-url', + appRoute: '/app/app2', id: 'app2', - legacy: true, navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, }) @@ -558,39 +452,19 @@ describe('#start()', () => { navLinks: { app1: true, app2: false, - legacyApp1: true, - legacyApp2: false, }, }, } as any); - const { register, registerLegacyApp } = service.setup(setupDeps); + const { register } = service.setup(setupDeps); register(Symbol(), createApp({ id: 'app1' })); - registerLegacyApp(createLegacyApp({ id: 'legacyApp1' })); register(Symbol(), createApp({ id: 'app2' })); - registerLegacyApp(createLegacyApp({ id: 'legacyApp2' })); const { applications$ } = await service.start(startDeps); const availableApps = await applications$.pipe(take(1)).toPromise(); - expect([...availableApps.keys()]).toEqual(['app1', 'legacyApp1']); - }); - - describe('currentAppId$', () => { - it('emits the legacy app id when in legacy mode', async () => { - setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true); - setupDeps.injectedMetadata.getLegacyMetadata.mockReturnValue({ - app: { - id: 'legacy', - title: 'Legacy App', - }, - } as any); - await service.setup(setupDeps); - const { currentAppId$ } = await service.start(startDeps); - - expect(await currentAppId$.pipe(take(1)).toPromise()).toEqual('legacy'); - }); + expect([...availableApps.keys()]).toEqual(['app1']); }); describe('getComponent', () => { @@ -602,16 +476,6 @@ describe('#start()', () => { expect(() => shallow(createElement(getComponent))).not.toThrow(); expect(getComponent()).toMatchSnapshot(); }); - - it('renders null when in legacy mode', async () => { - setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true); - service.setup(setupDeps); - - const { getComponent } = await service.start(startDeps); - - expect(() => shallow(createElement(getComponent))).not.toThrow(); - expect(getComponent()).toBe(null); - }); }); describe('getUrlForApp', () => { @@ -624,16 +488,14 @@ describe('#start()', () => { }); it('creates URL for registered appId', async () => { - const { register, registerLegacyApp } = service.setup(setupDeps); + const { register } = service.setup(setupDeps); register(Symbol(), createApp({ id: 'app1' })); - registerLegacyApp(createLegacyApp({ id: 'legacyApp1' })); register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/path' })); const { getUrlForApp } = await service.start(startDeps); expect(getUrlForApp('app1')).toBe('/base-path/app/app1'); - expect(getUrlForApp('legacyApp1')).toBe('/base-path/app/legacyApp1'); expect(getUrlForApp('app2')).toBe('/base-path/custom/path'); }); @@ -800,16 +662,6 @@ describe('#start()', () => { expect(MockHistory.push).toHaveBeenCalledWith('/custom/path', 'my-state'); }); - it('redirects when in legacyMode', async () => { - setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true); - service.setup(setupDeps); - - const { navigateToApp } = await service.start(startDeps); - - await navigateToApp('myTestApp'); - expect(setupDeps.redirectTo).toHaveBeenCalledWith('/base-path/app/myTestApp'); - }); - it('updates currentApp$ after mounting', async () => { service.setup(setupDeps); @@ -903,31 +755,6 @@ describe('#start()', () => { `); }); - it('sets window.location.href when navigating to legacy apps', async () => { - setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' }); - setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true); - service.setup(setupDeps); - - const { navigateToApp } = await service.start(startDeps); - - await navigateToApp('alpha'); - expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/alpha'); - }); - - it('handles legacy apps with subapps', async () => { - setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' }); - setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true); - - const { registerLegacyApp } = service.setup(setupDeps); - - registerLegacyApp(createLegacyApp({ id: 'baseApp:legacyApp1' })); - - const { navigateToApp } = await service.start(startDeps); - - await navigateToApp('baseApp:legacyApp1'); - expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/baseApp'); - }); - describe('when `replace` option is true', () => { it('use `history.replace` instead of `history.push`', async () => { service.setup(setupDeps); @@ -973,16 +800,6 @@ describe('#start()', () => { undefined ); }); - it('do not change the behavior when in legacy mode', async () => { - setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' }); - setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true); - service.setup(setupDeps); - - const { navigateToApp } = await service.start(startDeps); - - await navigateToApp('alpha', { replace: true }); - expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/alpha'); - }); }); describe('when `replace` option is false', () => { @@ -1040,9 +857,7 @@ describe('#stop()', () => { setupDeps = { http, context: contextServiceMock.createSetupContract(), - injectedMetadata: injectedMetadataServiceMock.createSetupContract(), }; - setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false); startDeps = { http, overlays: overlayServiceMock.createStartContract() }; service = new ApplicationService(); }); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index df0f74c1914e9..0d08f6f3007b0 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -23,7 +23,6 @@ import { map, shareReplay, takeUntil, distinctUntilChanged, filter } from 'rxjs/ import { createBrowserHistory, History } from 'history'; import { MountPoint } from '../types'; -import { InjectedMetadataSetup } from '../injected_metadata'; import { HttpSetup, HttpStart } from '../http'; import { OverlayStart } from '../overlays'; import { ContextSetup, IContextContainer } from '../context'; @@ -32,7 +31,6 @@ import { AppRouter } from './ui'; import { Capabilities, CapabilitiesService } from './capabilities'; import { App, - AppBase, AppLeaveHandler, AppMount, AppMountDeprecated, @@ -42,8 +40,6 @@ import { AppUpdater, InternalApplicationSetup, InternalApplicationStart, - LegacyApp, - LegacyAppMounter, Mounter, NavigateToAppOptions, } from './types'; @@ -53,9 +49,8 @@ import { appendAppPath, parseAppUrl, relativeToAbsolute, getAppInfo } from './ut interface SetupDeps { context: ContextSetup; http: HttpSetup; - injectedMetadata: InjectedMetadataSetup; history?: History; - /** Used to redirect to external urls (and legacy apps) */ + /** Used to redirect to external urls */ redirectTo?: (path: string) => void; } @@ -101,7 +96,7 @@ interface AppInternalState { * @internal */ export class ApplicationService { - private readonly apps = new Map | LegacyApp>(); + private readonly apps = new Map>(); private readonly mounters = new Map(); private readonly capabilities = new CapabilitiesService(); private readonly appInternalStates = new Map(); @@ -119,28 +114,17 @@ export class ApplicationService { public setup({ context, http: { basePath }, - injectedMetadata, redirectTo = (path: string) => { window.location.assign(path); }, history, }: SetupDeps): InternalApplicationSetup { const basename = basePath.get(); - if (injectedMetadata.getLegacyMode()) { - this.currentAppId$.next(injectedMetadata.getLegacyMetadata().app.id); - } else { - // Only setup history if we're not in legacy mode - this.history = history || createBrowserHistory({ basename }); - } + this.history = history || createBrowserHistory({ basename }); this.navigate = (url, state, replace) => { - if (this.history) { - // basePath not needed here because `history` is configured with basename - return replace ? this.history.replace(url, state) : this.history.push(url, state); - } else { - // If we do not have history available (legacy mode), use redirectTo to do a full page refresh. - return redirectTo(basePath.prepend(url)); - } + // basePath not needed here because `history` is configured with basename + return replace ? this.history!.replace(url, state) : this.history!.push(url, state); }; this.redirectTo = redirectTo; @@ -200,7 +184,6 @@ export class ApplicationService { ...appProps, status: app.status ?? AppStatus.accessible, navLinkStatus: app.navLinkStatus ?? AppNavLinkStatus.default, - legacy: false, }); if (updater$) { registerStatusUpdater(app.id, updater$); @@ -211,43 +194,6 @@ export class ApplicationService { exactRoute: app.exactRoute ?? false, mount: wrapMount(plugin, app), unmountBeforeMounting: false, - legacy: false, - }); - }, - registerLegacyApp: (app) => { - const appRoute = `/app/${app.id.split(':')[0]}`; - - if (this.registrationClosed) { - throw new Error('Applications cannot be registered after "setup"'); - } else if (this.apps.has(app.id)) { - throw new Error(`An application is already registered with the id "${app.id}"`); - } else if (basename && appRoute!.startsWith(`${basename}/`)) { - throw new Error('Cannot register an application route that includes HTTP base path'); - } - - const appBasePath = basePath.prepend(appRoute); - const mount: LegacyAppMounter = ({ history: appHistory }) => { - redirectTo(appHistory.createHref(appHistory.location)); - window.location.reload(); - }; - - const { updater$, ...appProps } = app; - this.apps.set(app.id, { - ...appProps, - status: app.status ?? AppStatus.accessible, - navLinkStatus: app.navLinkStatus ?? AppNavLinkStatus.default, - legacy: true, - }); - if (updater$) { - registerStatusUpdater(app.id, updater$); - } - this.mounters.set(app.id, { - appRoute, - appBasePath, - exactRoute: false, - mount, - unmountBeforeMounting: true, - legacy: true, }); }, registerAppUpdater: (appUpdater$: Observable) => @@ -323,7 +269,7 @@ export class ApplicationService { distinctUntilChanged(), takeUntil(this.stop$) ), - history: this.history, + history: this.history!, registerMountContext: this.mountContext.registerContext, getUrlForApp: ( appId, @@ -421,7 +367,7 @@ export class ApplicationService { } } -const updateStatus = (app: T, statusUpdaters: AppUpdaterWrapper[]): T => { +const updateStatus = (app: App, statusUpdaters: AppUpdaterWrapper[]): App => { let changes: Partial = {}; statusUpdaters.forEach((wrapper) => { if (wrapper.application !== allApplicationsFilter && wrapper.application !== app.id) { diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index 121f0c7ac07d6..4f3b113a29c9b 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -22,7 +22,6 @@ export { Capabilities } from './capabilities'; export { ScopedHistory } from './scoped_history'; export { App, - AppBase, AppMount, AppMountDeprecated, AppUnmount, @@ -39,10 +38,8 @@ export { AppLeaveAction, AppLeaveDefaultAction, AppLeaveConfirmAction, - LegacyApp, NavigateToAppOptions, PublicAppInfo, - PublicLegacyAppInfo, // Internal types InternalApplicationSetup, InternalApplicationStart, diff --git a/src/core/public/application/integration_tests/application_service.test.tsx b/src/core/public/application/integration_tests/application_service.test.tsx index 9eafddd6a61fe..d28486928b7e2 100644 --- a/src/core/public/application/integration_tests/application_service.test.tsx +++ b/src/core/public/application/integration_tests/application_service.test.tsx @@ -25,11 +25,9 @@ import { createRenderer } from './utils'; import { ApplicationService } from '../application_service'; import { httpServiceMock } from '../../http/http_service.mock'; import { contextServiceMock } from '../../context/context_service.mock'; -import { injectedMetadataServiceMock } from '../../injected_metadata/injected_metadata_service.mock'; import { MockLifecycle } from '../test_types'; import { overlayServiceMock } from '../../overlays/overlay_service.mock'; import { AppMountParameters } from '../types'; -import { ScopedHistory } from '../scoped_history'; import { Observable } from 'rxjs'; import { MountPoint } from 'kibana/public'; @@ -56,10 +54,8 @@ describe('ApplicationService', () => { setupDeps = { http, context: contextServiceMock.createSetupContract(), - injectedMetadata: injectedMetadataServiceMock.createSetupContract(), history: history as any, }; - setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false); startDeps = { http, overlays: overlayServiceMock.createStartContract() }; service = new ApplicationService(); }); @@ -149,54 +145,6 @@ describe('ApplicationService', () => { }); }); - describe('redirects', () => { - beforeAll(() => { - Object.defineProperty(window, 'location', { - value: { - reload: jest.fn(), - }, - }); - }); - - it('to full path when navigating to legacy app', async () => { - const redirectTo = jest.fn(); - - // In the real application, we use a BrowserHistory instance configured with `basename`. However, in tests we must - // use MemoryHistory which does not support `basename`. In order to emulate this behavior, we will wrap this - // instance with a ScopedHistory configured with a basepath. - history.push(setupDeps.http.basePath.get()); // ScopedHistory constructor will fail if underlying history is not currently at basePath. - const { register, registerLegacyApp } = service.setup({ - ...setupDeps, - redirectTo, - history: new ScopedHistory(history, setupDeps.http.basePath.get()), - }); - - register(Symbol(), { - id: 'app1', - title: 'App1', - mount: ({ onAppLeave }: AppMountParameters) => { - onAppLeave((actions) => actions.default()); - return () => undefined; - }, - }); - registerLegacyApp({ - id: 'myLegacyTestApp', - appUrl: '/app/myLegacyTestApp', - title: 'My Legacy Test App', - }); - - const { navigateToApp, getComponent } = await service.start(startDeps); - - update = createRenderer(getComponent()); - - await navigate('/test/app/app1'); - await act(() => navigateToApp('myLegacyTestApp', { path: '#/some-path' })); - - expect(redirectTo).toHaveBeenCalledWith('/test/app/myLegacyTestApp#/some-path'); - expect(window.location.reload).toHaveBeenCalled(); - }); - }); - describe('leaving an application that registered an app leave handler', () => { it('navigates to the new app if action is default', async () => { startDeps.overlays.openConfirm.mockResolvedValue(true); diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index 6408b8123365e..e3f992990f9f9 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -22,13 +22,12 @@ import { BehaviorSubject } from 'rxjs'; import { createMemoryHistory, History, createHashHistory } from 'history'; import { AppRouter, AppNotFound } from '../ui'; -import { EitherApp, MockedMounterMap, MockedMounterTuple } from '../test_types'; -import { createRenderer, createAppMounter, createLegacyAppMounter, getUnmounter } from './utils'; +import { MockedMounterMap, MockedMounterTuple } from '../test_types'; +import { createRenderer, createAppMounter, getUnmounter } from './utils'; import { AppStatus } from '../types'; -import { ScopedHistory } from '../scoped_history'; describe('AppRouter', () => { - let mounters: MockedMounterMap; + let mounters: MockedMounterMap; let globalHistory: History; let update: ReturnType; let scopedAppHistory: History; @@ -67,9 +66,7 @@ describe('AppRouter', () => { beforeEach(() => { mounters = new Map([ createAppMounter({ appId: 'app1', html: 'App 1' }), - createLegacyAppMounter('legacyApp1', jest.fn()), createAppMounter({ appId: 'app2', html: '
App 2
' }), - createLegacyAppMounter('baseApp:legacyApp2', jest.fn()), createAppMounter({ appId: 'app3', html: '
Chromeless A
', @@ -81,7 +78,6 @@ describe('AppRouter', () => { appRoute: '/chromeless-b/path', }), createAppMounter({ appId: 'disabledApp', html: '
Disabled app
' }), - createLegacyAppMounter('disabledLegacyApp', jest.fn()), createAppMounter({ appId: 'scopedApp', extraMountHook: ({ history }) => { @@ -99,7 +95,7 @@ describe('AppRouter', () => { html: '
App 6
', appRoute: '/app/my-app/app6', }), - ] as Array>); + ] as MockedMounterTuple[]); globalHistory = createMemoryHistory(); update = createMountersRenderer(); }); @@ -384,26 +380,6 @@ describe('AppRouter', () => { expect(globalHistory.location.pathname).toEqual('/app/scopedApp/subpath'); }); - it('calls legacy mount handler', async () => { - await navigate('/app/legacyApp1'); - expect(mounters.get('legacyApp1')!.mounter.mount.mock.calls[0][0]).toMatchObject({ - appBasePath: '/app/legacyApp1', - element: expect.any(HTMLDivElement), - onAppLeave: expect.any(Function), - history: expect.any(ScopedHistory), - }); - }); - - it('handles legacy apps with subapps', async () => { - await navigate('/app/baseApp'); - expect(mounters.get('baseApp:legacyApp2')!.mounter.mount.mock.calls[0][0]).toMatchObject({ - appBasePath: '/app/baseApp', - element: expect.any(HTMLDivElement), - onAppLeave: expect.any(Function), - history: expect.any(ScopedHistory), - }); - }); - it('displays error page if no app is found', async () => { const dom = await navigate('/app/unknown'); @@ -415,10 +391,4 @@ describe('AppRouter', () => { expect(dom?.exists(AppNotFound)).toBe(true); }); - - it('displays error page if legacy app is inaccessible', async () => { - const dom = await navigate('/app/disabledLegacyApp'); - - expect(dom?.exists(AppNotFound)).toBe(true); - }); }); diff --git a/src/core/public/application/integration_tests/utils.tsx b/src/core/public/application/integration_tests/utils.tsx index 80a7fc2c2cad6..2ed9e0c495fb9 100644 --- a/src/core/public/application/integration_tests/utils.tsx +++ b/src/core/public/application/integration_tests/utils.tsx @@ -20,11 +20,10 @@ import React, { ReactElement } from 'react'; import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; - import { I18nProvider } from '@kbn/i18n/react'; -import { App, LegacyApp, AppMountParameters } from '../types'; -import { EitherApp, MockedMounter, MockedMounterTuple, Mountable } from '../test_types'; +import { AppMountParameters } from '../types'; +import { MockedMounterTuple, Mountable } from '../test_types'; type Dom = ReturnType | null; type Renderer = () => Dom | Promise; @@ -55,7 +54,7 @@ export const createAppMounter = ({ appRoute?: string; exactRoute?: boolean; extraMountHook?: (params: AppMountParameters) => void; -}): MockedMounterTuple => { +}): MockedMounterTuple => { const unmount = jest.fn(); return [ appId, @@ -63,7 +62,6 @@ export const createAppMounter = ({ mounter: { appRoute, appBasePath: appRoute, - legacy: false, exactRoute, mount: jest.fn(async (params: AppMountParameters) => { const { appBasePath: basename, element } = params; @@ -82,24 +80,6 @@ export const createAppMounter = ({ ]; }; -export const createLegacyAppMounter = ( - appId: string, - legacyMount: MockedMounter['mount'] -): MockedMounterTuple => [ - appId, - { - mounter: { - appRoute: `/app/${appId.split(':')[0]}`, - appBasePath: `/app/${appId.split(':')[0]}`, - unmountBeforeMounting: true, - legacy: true, - exactRoute: false, - mount: legacyMount, - }, - unmount: jest.fn(), - }, -]; - -export function getUnmounter(app: Mountable) { +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 b822597e510cb..64012f0c0b6c1 100644 --- a/src/core/public/application/test_types.ts +++ b/src/core/public/application/test_types.ts @@ -17,28 +17,26 @@ * under the License. */ -import { App, LegacyApp, Mounter, AppUnmount } from './types'; +import { AppUnmount, Mounter } from './types'; import { ApplicationService } from './application_service'; /** @internal */ export type ApplicationServiceContract = PublicMethodsOf; /** @internal */ -export type EitherApp = App | LegacyApp; -/** @internal */ export type MockedUnmount = jest.Mocked; /** @internal */ -export interface Mountable { - mounter: MockedMounter; +export interface Mountable { + mounter: MockedMounter; unmount: MockedUnmount; } /** @internal */ -export type MockedMounter = jest.Mocked>>; +export type MockedMounter = jest.Mocked; /** @internal */ -export type MockedMounterTuple = [string, Mountable]; +export type MockedMounterTuple = [string, Mountable]; /** @internal */ -export type MockedMounterMap = Map>; +export type MockedMounterMap = Map; /** @internal */ export type MockLifecycle< T extends keyof ApplicationService, diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 320416a8c2379..df83b6e932aad 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -36,8 +36,64 @@ import { SavedObjectsStart } from '../saved_objects'; import { AppCategory } from '../../types'; import { ScopedHistory } from './scoped_history'; -/** @public */ -export interface AppBase { +/** + * Accessibility status of an application. + * + * @public + */ +export enum AppStatus { + /** + * Application is accessible. + */ + accessible = 0, + /** + * Application is not accessible. + */ + inaccessible = 1, +} + +/** + * Status of the application's navLink. + * + * @public + */ +export enum AppNavLinkStatus { + /** + * The application navLink will be `visible` if the application's {@link AppStatus} is set to `accessible` + * and `hidden` if the application status is set to `inaccessible`. + */ + default = 0, + /** + * The application navLink is visible and clickable in the navigation bar. + */ + visible = 1, + /** + * The application navLink is visible but inactive and not clickable in the navigation bar. + */ + disabled = 2, + /** + * The application navLink does not appear in the navigation bar. + */ + hidden = 3, +} + +/** + * Defines the list of fields that can be updated via an {@link AppUpdater}. + * @public + */ +export type AppUpdatableFields = Pick; + +/** + * Updater for applications. + * see {@link ApplicationSetup} + * @public + */ +export type AppUpdater = (app: App) => Partial | undefined; + +/** + * @public + */ +export interface App { /** * The unique identifier of the application */ @@ -136,83 +192,12 @@ export interface AppBase { */ capabilities?: Partial; - /** - * Flag to keep track of legacy applications. - * For internal use only. any value will be overridden when registering an App. - * - * @internal - */ - legacy?: boolean; - /** * Hide the UI chrome when the application is mounted. Defaults to `false`. * Takes precedence over chrome service visibility settings. */ chromeless?: boolean; -} -/** - * Accessibility status of an application. - * - * @public - */ -export enum AppStatus { - /** - * Application is accessible. - */ - accessible = 0, - /** - * Application is not accessible. - */ - inaccessible = 1, -} - -/** - * Status of the application's navLink. - * - * @public - */ -export enum AppNavLinkStatus { - /** - * The application navLink will be `visible` if the application's {@link AppStatus} is set to `accessible` - * and `hidden` if the application status is set to `inaccessible`. - */ - default = 0, - /** - * The application navLink is visible and clickable in the navigation bar. - */ - visible = 1, - /** - * The application navLink is visible but inactive and not clickable in the navigation bar. - */ - disabled = 2, - /** - * The application navLink does not appear in the navigation bar. - */ - hidden = 3, -} - -/** - * Defines the list of fields that can be updated via an {@link AppUpdater}. - * @public - */ -export type AppUpdatableFields = Pick< - AppBase, - 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' ->; - -/** - * Updater for applications. - * see {@link ApplicationSetup} - * @public - */ -export type AppUpdater = (app: AppBase) => Partial | undefined; - -/** - * Extension of {@link AppBase | common app properties} with the mount function. - * @public - */ -export interface App extends AppBase { /** * A mount function called when the user navigates to this app's route. May have signature of {@link AppMount} or * {@link AppMountDeprecated}. @@ -223,12 +208,6 @@ export interface App extends AppBase { */ mount: AppMount | AppMountDeprecated; - /** - * Hide the UI chrome when the application is mounted. Defaults to `false`. - * Takes precedence over chrome service visibility settings. - */ - chromeless?: boolean; - /** * Override the application's routing path from `/app/${id}`. * Must be unique across registered applications. Should not include the @@ -255,39 +234,18 @@ export interface App extends AppBase { exactRoute?: boolean; } -/** @public */ -export interface LegacyApp extends AppBase { - appUrl: string; - subUrlBase?: string; - linkToLastSubUrl?: boolean; - disableSubUrlTracking?: boolean; -} - /** * Public information about a registered {@link App | application} * * @public */ export type PublicAppInfo = Omit & { - legacy: false; // remove optional on fields populated with default values status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; }; -/** - * Information about a registered {@link LegacyApp | legacy application} - * - * @public - */ -export type PublicLegacyAppInfo = Omit & { - legacy: true; - // remove optional on fields populated with default values - status: AppStatus; - navLinkStatus: AppNavLinkStatus; -}; - /** * A mount function called when the user navigates to this app's route. * @@ -300,6 +258,12 @@ export type AppMount = ( params: AppMountParameters ) => AppUnmount | Promise; +/** + * A function called when an application should be unmounted from the page. This function should be synchronous. + * @public + */ +export type AppUnmount = () => void; + /** * A mount function called when the user navigates to this app's route. * @@ -607,30 +571,14 @@ export interface AppLeaveActionFactory { default(): AppLeaveDefaultAction; } -/** - * A function called when an application should be unmounted from the page. This function should be synchronous. - * @public - */ -export type AppUnmount = () => void; - -/** @internal */ -export type AppMounter = (params: AppMountParameters) => Promise; - -/** @internal */ -export type LegacyAppMounter = (params: AppMountParameters) => void; - /** @internal */ -export type Mounter = SelectivePartial< - { - appRoute: string; - appBasePath: string; - mount: T extends LegacyApp ? LegacyAppMounter : AppMounter; - legacy: boolean; - exactRoute: boolean; - unmountBeforeMounting: T extends LegacyApp ? true : boolean; - }, - T extends LegacyApp ? never : 'unmountBeforeMounting' ->; +export interface Mounter { + appRoute: string; + appBasePath: string; + mount: AppMount; + exactRoute: boolean; + unmountBeforeMounting?: boolean; +} /** @internal */ export interface ParsedAppUrl { @@ -702,13 +650,6 @@ export interface InternalApplicationSetup extends Pick ): void; - /** - * Register metadata about legacy applications. Legacy apps will not be mounted when navigated to. - * @param app - * @internal - */ - registerLegacyApp(app: LegacyApp): void; - /** * Register a context provider for application mounting. Will only be available to applications that depend on the * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. @@ -731,7 +672,7 @@ export interface InternalApplicationSetup extends Pick>; + applications$: Observable>; /** * Navigate to a given app @@ -861,14 +799,8 @@ export interface InternalApplicationStart extends Omit; /** - * The global history instance, exposed only to Core. Undefined when rendering a legacy application. + * The global history instance, exposed only to Core. * @internal */ - history: History | undefined; + history: History; } - -/** @internal */ -type SelectivePartial = Partial> & - Required>> extends infer U - ? { [P in keyof U]: U[P] } - : never; diff --git a/src/core/public/application/ui/app_container.test.tsx b/src/core/public/application/ui/app_container.test.tsx index e26fe7e59fd04..f6cde54e6f502 100644 --- a/src/core/public/application/ui/app_container.test.tsx +++ b/src/core/public/application/ui/app_container.test.tsx @@ -55,7 +55,6 @@ describe('AppContainer', () => { appBasePath: '/base-path', appRoute: '/some-route', unmountBeforeMounting: false, - legacy: false, exactRoute: false, mount: async ({ element }: AppMountParameters) => { await promise; @@ -146,7 +145,6 @@ describe('AppContainer', () => { appBasePath: '/base-path/some-route', appRoute: '/some-route', unmountBeforeMounting: false, - legacy: false, exactRoute: false, mount: async ({ element }: AppMountParameters) => { await waitPromise; diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index 5021dd3ae765a..42bc9a53aee2d 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -58,25 +58,21 @@ export const AppRouter: FunctionComponent = ({ return ( - {[...mounters] - // legacy apps can have multiple sub-apps registered with the same route - // which needs additional logic that is handled in the catch-all route below - .filter(([_, mounter]) => !mounter.legacy) - .map(([appId, mounter]) => ( - ( - - )} - /> - ))} + {[...mounters].map(([appId, mounter]) => ( + ( + + )} + /> + ))} {/* handler for legacy apps and used as a catch-all to display 404 page on not existing /app/appId apps*/} = ({ url, }, }: RouteComponentProps) => { - // Find the mounter including legacy mounters with subapps: - const [id, mounter] = mounters.has(appId) - ? [appId, mounters.get(appId)] - : [...mounters].filter(([key]) => key.split(':')[0] === appId)[0] ?? []; - + // the id/mounter retrieval can be removed once #76348 is addressed + const [id, mounter] = mounters.has(appId) ? [appId, mounters.get(appId)] : []; return ( diff --git a/src/core/public/application/utils.test.ts b/src/core/public/application/utils.test.ts index 4663ca2db21e7..ee1d82a7a872e 100644 --- a/src/core/public/application/utils.test.ts +++ b/src/core/public/application/utils.test.ts @@ -18,16 +18,9 @@ */ import { of } from 'rxjs'; -import { App, AppNavLinkStatus, AppStatus, LegacyApp } from './types'; +import { App, AppNavLinkStatus, AppStatus } from './types'; import { BasePath } from '../http/base_path'; -import { - appendAppPath, - getAppInfo, - isLegacyApp, - parseAppUrl, - relativeToAbsolute, - removeSlashes, -} from './utils'; +import { appendAppPath, getAppInfo, parseAppUrl, relativeToAbsolute, removeSlashes } from './utils'; describe('removeSlashes', () => { it('only removes duplicates by default', () => { @@ -70,9 +63,11 @@ describe('appendAppPath', () => { expect(appendAppPath('/app/my-app', '')).toEqual('/app/my-app'); }); - it('preserves the trailing slash only if included in the hash', () => { + it('preserves the trailing slash only if included in the hash or appPath', () => { expect(appendAppPath('/app/my-app', '/some-path/')).toEqual('/app/my-app/some-path'); expect(appendAppPath('/app/my-app', '/some-path#/')).toEqual('/app/my-app/some-path#/'); + expect(appendAppPath('/app/my-app#/', '')).toEqual('/app/my-app#/'); + expect(appendAppPath('/app/my-app#', '/')).toEqual('/app/my-app#/'); expect(appendAppPath('/app/my-app', '/some-path#/hash/')).toEqual( '/app/my-app/some-path#/hash/' ); @@ -80,29 +75,6 @@ describe('appendAppPath', () => { }); }); -describe('isLegacyApp', () => { - it('returns true for legacy apps', () => { - expect( - isLegacyApp({ - id: 'legacy', - title: 'Legacy App', - appUrl: '/some-url', - legacy: true, - }) - ).toEqual(true); - }); - it('returns false for non-legacy apps', () => { - expect( - isLegacyApp({ - id: 'legacy', - title: 'Legacy App', - mount: () => () => undefined, - legacy: false, - }) - ).toEqual(false); - }); -}); - describe('relativeToAbsolute', () => { it('converts a relative path to an absolute url', () => { const origin = window.location.origin; @@ -113,7 +85,7 @@ describe('relativeToAbsolute', () => { }); describe('parseAppUrl', () => { - let apps: Map | LegacyApp>; + let apps: Map>; let basePath: BasePath; const getOrigin = () => 'https://kibana.local:8080'; @@ -124,19 +96,6 @@ describe('parseAppUrl', () => { title: 'some-title', mount: () => () => undefined, ...props, - legacy: false, - }; - apps.set(app.id, app); - return app; - }; - - const createLegacyApp = (props: Partial): LegacyApp => { - const app: LegacyApp = { - id: 'some-id', - title: 'some-title', - appUrl: '/my-url', - ...props, - legacy: true, }; apps.set(app.id, app); return app; @@ -153,10 +112,6 @@ describe('parseAppUrl', () => { id: 'bar', appRoute: '/custom-bar', }); - createLegacyApp({ - id: 'legacy', - appUrl: '/app/legacy', - }); }); describe('with relative paths', () => { @@ -236,18 +191,6 @@ describe('parseAppUrl', () => { path: '/path#hash/bang?hello=dolly', }); }); - it('works with legacy apps', () => { - expect(parseAppUrl('/base-path/app/legacy', basePath, apps, getOrigin)).toEqual({ - app: 'legacy', - path: undefined, - }); - expect( - parseAppUrl('/base-path/app/legacy/path#hash?query=bar', basePath, apps, getOrigin) - ).toEqual({ - app: 'legacy', - path: '/path#hash?query=bar', - }); - }); it('returns undefined when the app is not known', () => { expect(parseAppUrl('/base-path/app/non-registered', basePath, apps, getOrigin)).toEqual( undefined @@ -409,25 +352,6 @@ describe('parseAppUrl', () => { path: '/path#hash/bang?hello=dolly', }); }); - it('works with legacy apps', () => { - expect( - parseAppUrl('https://kibana.local:8080/base-path/app/legacy', basePath, apps, getOrigin) - ).toEqual({ - app: 'legacy', - path: undefined, - }); - expect( - parseAppUrl( - 'https://kibana.local:8080/base-path/app/legacy/path#hash?query=bar', - basePath, - apps, - getOrigin - ) - ).toEqual({ - app: 'legacy', - path: '/path#hash?query=bar', - }); - }); it('returns undefined when the app is not known', () => { expect( parseAppUrl( @@ -471,18 +395,6 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.default, appRoute: `/app/some-id`, - legacy: false, - ...props, - }); - - const createLegacyApp = (props: Partial = {}): LegacyApp => ({ - appUrl: '/my-app-url', - updater$: of(() => undefined), - id: 'some-id', - title: 'some-title', - status: AppStatus.accessible, - navLinkStatus: AppNavLinkStatus.default, - legacy: true, ...props, }); @@ -496,21 +408,6 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, - legacy: false, - }); - }); - - it('converts a legacy application and remove sensitive properties', () => { - const app = createLegacyApp(); - const info = getAppInfo(app); - - expect(info).toEqual({ - appUrl: '/my-app-url', - id: 'some-id', - title: 'some-title', - status: AppStatus.accessible, - navLinkStatus: AppNavLinkStatus.visible, - legacy: true, }); }); diff --git a/src/core/public/application/utils.ts b/src/core/public/application/utils.ts index c5ed7b659f3ae..85760526bf544 100644 --- a/src/core/public/application/utils.ts +++ b/src/core/public/application/utils.ts @@ -18,15 +18,7 @@ */ import { IBasePath } from '../http'; -import { - App, - AppNavLinkStatus, - AppStatus, - LegacyApp, - ParsedAppUrl, - PublicAppInfo, - PublicLegacyAppInfo, -} from './types'; +import { App, AppNavLinkStatus, AppStatus, ParsedAppUrl, PublicAppInfo } from './types'; /** * Utility to remove trailing, leading or duplicate slashes. @@ -55,8 +47,8 @@ export const removeSlashes = ( export const appendAppPath = (appBasePath: string, path: string = '') => { // Only prepend slash if not a hash or query path path = path === '' || path.startsWith('#') || path.startsWith('?') ? path : `/${path}`; - // Do not remove trailing slash when in hashbang - const removeTrailing = path.indexOf('#') === -1; + // Do not remove trailing slash when in hashbang or basePath + const removeTrailing = path.indexOf('#') === -1 && appBasePath.indexOf('#') === -1; return removeSlashes(`${appBasePath}${path}`, { trailing: removeTrailing, duplicates: true, @@ -64,10 +56,6 @@ export const appendAppPath = (appBasePath: string, path: string = '') => { }); }; -export function isLegacyApp(app: App | LegacyApp): app is LegacyApp { - return app.legacy === true; -} - /** * Converts a relative path to an absolute url. * Implementation is based on a specified behavior of the browser to automatically convert @@ -95,7 +83,7 @@ export const relativeToAbsolute = (url: string): string => { export const parseAppUrl = ( url: string, basePath: IBasePath, - apps: Map | LegacyApp>, + apps: Map>, getOrigin: () => string = () => window.location.origin ): ParsedAppUrl | undefined => { url = removeBasePath(url, basePath, getOrigin()); @@ -104,7 +92,7 @@ export const parseAppUrl = ( } for (const app of apps.values()) { - const appPath = isLegacyApp(app) ? app.appUrl : app.appRoute || `/app/${app.id}`; + const appPath = app.appRoute || `/app/${app.id}`; if (url.startsWith(appPath)) { const path = url.substr(appPath.length); @@ -123,29 +111,18 @@ const removeBasePath = (url: string, basePath: IBasePath, origin: string): strin return basePath.remove(url); }; -export function getAppInfo(app: App | LegacyApp): PublicAppInfo | PublicLegacyAppInfo { +export function getAppInfo(app: App): PublicAppInfo { const navLinkStatus = app.navLinkStatus === AppNavLinkStatus.default ? app.status === AppStatus.inaccessible ? AppNavLinkStatus.hidden : AppNavLinkStatus.visible : app.navLinkStatus!; - if (isLegacyApp(app)) { - const { updater$, ...infos } = app; - return { - ...infos, - status: app.status!, - navLinkStatus, - legacy: true, - }; - } else { - const { updater$, mount, ...infos } = app; - return { - ...infos, - status: app.status!, - navLinkStatus, - appRoute: app.appRoute!, - legacy: false, - }; - } + const { updater$, mount, ...infos } = app; + return { + ...infos, + status: app.status!, + navLinkStatus, + appRoute: app.appRoute!, + }; } diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index c9a05ff4e08fe..5862ee7175f71 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -47,9 +47,6 @@ const createStartContractMock = () => { docTitle: { change: jest.fn(), reset: jest.fn(), - __legacy: { - setBaseTitle: jest.fn(), - }, }, navControls: { registerLeft: jest.fn(), diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index ef9a682d609ec..b96c34cd9fbe8 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -238,7 +238,6 @@ export class ChromeService { homeHref={http.basePath.prepend('/app/home')} isVisible$={this.isVisible$} kibanaVersion={injectedMetadata.getKibanaVersion()} - legacyMode={injectedMetadata.getLegacyMode()} navLinks$={navLinks.getNavLinks$()} recentlyAccessed$={recentlyAccessed.get$()} navControlsLeft$={navControls.getLeft$()} diff --git a/src/core/public/chrome/doc_title/doc_title_service.test.ts b/src/core/public/chrome/doc_title/doc_title_service.test.ts index 763e8c9ebd74a..953baf7e7d1d5 100644 --- a/src/core/public/chrome/doc_title/doc_title_service.test.ts +++ b/src/core/public/chrome/doc_title/doc_title_service.test.ts @@ -64,17 +64,4 @@ describe('DocTitleService', () => { expect(document.title).toEqual('InitialTitle'); }); }); - - describe('#__legacy.setBaseTitle()', () => { - it('allows to change the baseTitle after startup', async () => { - const start = getStart('InitialTitle'); - start.change('WithInitial'); - expect(document.title).toEqual('WithInitial - InitialTitle'); - start.__legacy.setBaseTitle('NewBaseTitle'); - start.change('WithNew'); - expect(document.title).toEqual('WithNew - NewBaseTitle'); - start.reset(); - expect(document.title).toEqual('NewBaseTitle'); - }); - }); }); diff --git a/src/core/public/chrome/doc_title/doc_title_service.ts b/src/core/public/chrome/doc_title/doc_title_service.ts index c6e9ec7a40b77..817a460acaf3f 100644 --- a/src/core/public/chrome/doc_title/doc_title_service.ts +++ b/src/core/public/chrome/doc_title/doc_title_service.ts @@ -59,11 +59,6 @@ export interface ChromeDocTitle { * (meaning the one present in the title meta at application load.) */ reset(): void; - - /** @internal */ - __legacy: { - setBaseTitle(baseTitle: string): void; - }; } const defaultTitle: string[] = []; @@ -85,11 +80,6 @@ export class DocTitleService { reset: () => { this.applyTitle(defaultTitle); }, - __legacy: { - setBaseTitle: (baseTitle) => { - this.baseTitle = baseTitle; - }, - }, }; } diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index 55b5c80526bab..4b82e0ced4505 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -74,54 +74,8 @@ export interface ChromeNavLink { /** * Settled state between `url`, `baseUrl`, and `active` - * - * @internalRemarks - * This should be required once legacy apps are gone. - */ - readonly href?: string; - - /** LEGACY FIELDS */ - - /** - * A url base that legacy apps can set to match deep URLs to an application. - * - * @internalRemarks - * This should be removed once legacy apps are gone. - * - * @deprecated */ - readonly subUrlBase?: string; - - /** - * A flag that tells legacy chrome to ignore the link when - * tracking sub-urls - * - * @internalRemarks - * This should be removed once legacy apps are gone. - * - * @deprecated - */ - readonly disableSubUrlTracking?: boolean; - - /** - * Whether or not the subUrl feature should be enabled. - * - * @internalRemarks - * Only read by legacy platform. - * - * @deprecated - */ - readonly linkToLastSubUrl?: boolean; - - /** - * Indicates whether or not this app is currently on the screen. - * - * @internalRemarks - * Remove this when ApplicationService is implemented and managing apps. - * - * @deprecated - */ - readonly active?: boolean; + readonly href: string; /** * Disables a link from being clickable. @@ -129,30 +83,18 @@ export interface ChromeNavLink { * @internalRemarks * This is only used by the ML and Graph plugins currently. They use this field * to disable the nav link when the license is expired. - * - * @deprecated */ readonly disabled?: boolean; /** * Hides a link from the navigation. - * - * @internalRemarks - * Remove this when ApplicationService is implemented. Instead, plugins should only - * register an Application if needed. */ readonly hidden?: boolean; - - /** - * Used to separate links to legacy applications from NP applications - * @internal - */ - readonly legacy: boolean; } /** @public */ export type ChromeNavLinkUpdateableFields = Partial< - Pick + Pick >; export class NavLinkWrapper { @@ -170,7 +112,7 @@ export class NavLinkWrapper { public update(newProps: ChromeNavLinkUpdateableFields) { // Enforce limited properties at runtime for JS code - newProps = pick(newProps, ['active', 'disabled', 'hidden', 'url', 'subUrlBase', 'href']); + newProps = pick(newProps, ['disabled', 'hidden', 'url', 'href']); return new NavLinkWrapper({ ...this.properties, ...newProps }); } } diff --git a/src/core/public/chrome/nav_links/nav_links_service.test.ts b/src/core/public/chrome/nav_links/nav_links_service.test.ts index 8f610e238b0fd..a8413ed5b546a 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.test.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.test.ts @@ -19,7 +19,7 @@ import { NavLinksService } from './nav_links_service'; import { take, map, takeLast } from 'rxjs/operators'; -import { App, LegacyApp } from '../../application'; +import { App } from '../../application'; import { BehaviorSubject } from 'rxjs'; const availableApps = new Map([ @@ -34,32 +34,6 @@ const availableApps = new Map([ }, ], ['chromelessApp', { id: 'chromelessApp', order: 20, title: 'Chromless App', chromeless: true }], - [ - 'legacyApp1', - { - id: 'legacyApp1', - order: 5, - title: 'Legacy App 1', - icon: 'legacyApp1', - appUrl: '/app1', - legacy: true, - }, - ], - [ - 'legacyApp2', - { - id: 'legacyApp2', - order: -10, - title: 'Legacy App 2', - euiIconType: 'canvasApp', - appUrl: '/app2', - legacy: true, - }, - ], - [ - 'legacyApp3', - { id: 'legacyApp3', order: 20, title: 'Legacy App 3', appUrl: '/app3', legacy: true }, - ], ]); const mockHttp = { @@ -76,9 +50,7 @@ describe('NavLinksService', () => { beforeEach(() => { service = new NavLinksService(); mockAppService = { - applications$: new BehaviorSubject>( - availableApps as any - ), + applications$: new BehaviorSubject>(availableApps as any), }; start = service.start({ application: mockAppService, http: mockHttp }); }); @@ -105,19 +77,19 @@ describe('NavLinksService', () => { map((links) => links.map((l) => l.id)) ) .toPromise() - ).toEqual(['app2', 'legacyApp2', 'app1', 'legacyApp1', 'legacyApp3']); + ).toEqual(['app2', 'app1']); }); it('emits multiple values', async () => { const navLinkIds$ = start.getNavLinks$().pipe(map((links) => links.map((l) => l.id))); const emittedLinks: string[][] = []; navLinkIds$.subscribe((r) => emittedLinks.push(r)); - start.update('legacyApp1', { active: true }); + start.update('app1', { href: '/foo' }); service.stop(); expect(emittedLinks).toEqual([ - ['app2', 'legacyApp2', 'app1', 'legacyApp1', 'legacyApp3'], - ['app2', 'legacyApp2', 'app1', 'legacyApp1', 'legacyApp3'], + ['app2', 'app1'], + ['app2', 'app1'], ]); }); @@ -130,7 +102,7 @@ describe('NavLinksService', () => { describe('#get()', () => { it('returns link if exists', () => { - expect(start.get('legacyApp1')!.title).toEqual('Legacy App 1'); + expect(start.get('app2')!.title).toEqual('App 2'); }); it('returns undefined if it does not exist', () => { @@ -140,19 +112,13 @@ describe('NavLinksService', () => { describe('#getAll()', () => { it('returns a sorted array of navlinks', () => { - expect(start.getAll().map((l) => l.id)).toEqual([ - 'app2', - 'legacyApp2', - 'app1', - 'legacyApp1', - 'legacyApp3', - ]); + expect(start.getAll().map((l) => l.id)).toEqual(['app2', 'app1']); }); }); describe('#has()', () => { it('returns true if exists', () => { - expect(start.has('legacyApp1')).toBe(true); + expect(start.has('app2')).toBe(true); }); it('returns false if it does not exist', () => { @@ -171,7 +137,7 @@ describe('NavLinksService', () => { map((links) => links.map((l) => l.id)) ) .toPromise() - ).toEqual(['app2', 'legacyApp2', 'app1', 'legacyApp1', 'legacyApp3']); + ).toEqual(['app2', 'app1']); }); it('does nothing on chromeless applications', async () => { @@ -184,11 +150,11 @@ describe('NavLinksService', () => { map((links) => links.map((l) => l.id)) ) .toPromise() - ).toEqual(['app2', 'legacyApp2', 'app1', 'legacyApp1', 'legacyApp3']); + ).toEqual(['app2', 'app1']); }); it('removes all other links', async () => { - start.showOnly('legacyApp1'); + start.showOnly('app2'); expect( await start .getNavLinks$() @@ -197,11 +163,11 @@ describe('NavLinksService', () => { map((links) => links.map((l) => l.id)) ) .toPromise() - ).toEqual(['legacyApp1']); + ).toEqual(['app2']); }); it('still removes all other links when availableApps are re-emitted', async () => { - start.showOnly('legacyApp2'); + start.showOnly('app2'); mockAppService.applications$.next(mockAppService.applications$.value); expect( await start @@ -211,22 +177,19 @@ describe('NavLinksService', () => { map((links) => links.map((l) => l.id)) ) .toPromise() - ).toEqual(['legacyApp2']); + ).toEqual(['app2']); }); }); describe('#update()', () => { it('updates the navlinks and returns the updated link', async () => { - expect(start.update('legacyApp1', { hidden: true })).toEqual( + expect(start.update('app2', { hidden: true })).toEqual( expect.objectContaining({ - appUrl: '/app1', - disabled: false, hidden: true, - icon: 'legacyApp1', - id: 'legacyApp1', - legacy: true, - order: 5, - title: 'Legacy App 1', + id: 'app2', + order: -10, + title: 'App 2', + euiIconType: 'canvasApp', }) ); const hiddenLinkIds = await start @@ -236,7 +199,7 @@ describe('NavLinksService', () => { map((links) => links.filter((l) => l.hidden).map((l) => l.id)) ) .toPromise(); - expect(hiddenLinkIds).toEqual(['legacyApp1']); + expect(hiddenLinkIds).toEqual(['app2']); }); it('returns undefined if link does not exist', () => { @@ -244,7 +207,7 @@ describe('NavLinksService', () => { }); it('keeps the updated link when availableApps are re-emitted', async () => { - start.update('legacyApp1', { hidden: true }); + start.update('app2', { hidden: true }); mockAppService.applications$.next(mockAppService.applications$.value); const hiddenLinkIds = await start .getNavLinks$() @@ -253,7 +216,7 @@ describe('NavLinksService', () => { map((links) => links.filter((l) => l.hidden).map((l) => l.id)) ) .toPromise(); - expect(hiddenLinkIds).toEqual(['legacyApp1']); + expect(hiddenLinkIds).toEqual(['app2']); }); }); diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index ba04dbed49cd4..7e2c1fc1f89f8 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PublicAppInfo, AppNavLinkStatus, AppStatus, PublicLegacyAppInfo } from '../../application'; +import { PublicAppInfo, AppNavLinkStatus, AppStatus } from '../../application'; import { toNavLink } from './to_nav_link'; import { httpServiceMock } from '../../mocks'; @@ -28,17 +28,6 @@ const app = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.default, appRoute: `/app/some-id`, - legacy: false, - ...props, -}); - -const legacyApp = (props: Partial = {}): PublicLegacyAppInfo => ({ - appUrl: '/my-app-url', - id: 'some-id', - title: 'some-title', - status: AppStatus.accessible, - navLinkStatus: AppNavLinkStatus.default, - legacy: true, ...props, }); @@ -67,11 +56,6 @@ describe('toNavLink', () => { ); }); - it('flags legacy apps when converting to navLink', () => { - expect(toNavLink(app({}), basePath).properties.legacy).toEqual(false); - expect(toNavLink(legacyApp({}), basePath).properties.legacy).toEqual(true); - }); - it('handles applications with custom app route', () => { const link = toNavLink( app({ @@ -103,32 +87,6 @@ describe('toNavLink', () => { ); }); - it('does not generate `url` for legacy app', () => { - const link = toNavLink( - legacyApp({ - appUrl: '/my-legacy-app/#foo', - defaultPath: '/some/default/path', - }), - basePath - ); - expect(link.properties.url).toBeUndefined(); - }); - - it('uses appUrl when converting legacy applications', () => { - expect( - toNavLink( - legacyApp({ - appUrl: '/my-legacy-app/#foo', - }), - basePath - ).properties - ).toEqual( - expect.objectContaining({ - baseUrl: 'http://localhost/base-path/my-legacy-app/#foo', - }) - ); - }); - it('uses the application status when the navLinkStatus is set to default', () => { expect( toNavLink( diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts index 2dedbfd5f36ac..703c1798b6fb8 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.ts @@ -17,19 +17,14 @@ * under the License. */ -import { PublicAppInfo, AppNavLinkStatus, AppStatus, PublicLegacyAppInfo } from '../../application'; +import { PublicAppInfo, AppNavLinkStatus, AppStatus } from '../../application'; import { IBasePath } from '../../http'; import { NavLinkWrapper } from './nav_link'; import { appendAppPath } from '../../application/utils'; -export function toNavLink( - app: PublicAppInfo | PublicLegacyAppInfo, - basePath: IBasePath -): NavLinkWrapper { +export function toNavLink(app: PublicAppInfo, basePath: IBasePath): NavLinkWrapper { const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default; - const relativeBaseUrl = isLegacyApp(app) - ? basePath.prepend(app.appUrl) - : basePath.prepend(app.appRoute!); + const relativeBaseUrl = basePath.prepend(app.appRoute!); const url = relativeToAbsolute(appendAppPath(relativeBaseUrl, app.defaultPath)); const baseUrl = relativeToAbsolute(relativeBaseUrl); @@ -39,14 +34,9 @@ export function toNavLink( ? app.status === AppStatus.inaccessible : app.navLinkStatus === AppNavLinkStatus.hidden, disabled: useAppStatus ? false : app.navLinkStatus === AppNavLinkStatus.disabled, - legacy: isLegacyApp(app), baseUrl, - ...(isLegacyApp(app) - ? {} - : { - href: url, - url, - }), + href: url, + url, }); } @@ -63,7 +53,3 @@ export function relativeToAbsolute(url: string) { a.setAttribute('href', url); return a.href; } - -function isLegacyApp(app: PublicAppInfo | PublicLegacyAppInfo): app is PublicLegacyAppInfo { - return app.legacy === true; -} diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 79beaf79068ef..a770ece8496e4 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -71,7 +71,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "href": "Custom link", "id": "Custom link", "isActive": true, - "legacy": false, "title": "Custom link", }, "closed": false, @@ -123,7 +122,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` id="collapsibe-nav" isLocked={false} isOpen={true} - legacyMode={false} navLinks$={ BehaviorSubject { "_isScalar": false, @@ -140,7 +138,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "href": "discover", "id": "discover", "isActive": true, - "legacy": false, "title": "discover", }, Object { @@ -155,7 +152,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "href": "siem", "id": "siem", "isActive": true, - "legacy": false, "title": "siem", }, Object { @@ -170,7 +166,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "href": "metrics", "id": "metrics", "isActive": true, - "legacy": false, "title": "metrics", }, Object { @@ -184,7 +179,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "href": "monitoring", "id": "monitoring", "isActive": true, - "legacy": false, "title": "monitoring", }, Object { @@ -199,7 +193,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "href": "visualize", "id": "visualize", "isActive": true, - "legacy": false, "title": "visualize", }, Object { @@ -214,7 +207,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "href": "dashboard", "id": "dashboard", "isActive": true, - "legacy": false, "title": "dashboard", }, Object { @@ -224,7 +216,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "href": "canvas", "id": "canvas", "isActive": true, - "legacy": false, "title": "canvas", }, Object { @@ -239,7 +230,6 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` "href": "logs", "id": "logs", "isActive": true, - "legacy": false, "title": "logs", }, ], @@ -2116,7 +2106,6 @@ exports[`CollapsibleNav renders the default nav 1`] = ` id="collapsibe-nav" isLocked={false} isOpen={false} - legacyMode={false} navLinks$={ BehaviorSubject { "_isScalar": false, @@ -2351,7 +2340,6 @@ exports[`CollapsibleNav renders the default nav 2`] = ` id="collapsibe-nav" isLocked={false} isOpen={true} - legacyMode={false} navLinks$={ BehaviorSubject { "_isScalar": false, @@ -3038,7 +3026,6 @@ exports[`CollapsibleNav renders the default nav 3`] = ` id="collapsibe-nav" isLocked={true} isOpen={true} - legacyMode={false} navLinks$={ BehaviorSubject { "_isScalar": false, diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 5ec7a4773967b..128a0c5369e08 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -115,8 +115,8 @@ exports[`Header renders 1`] = ` "_isScalar": false, "_value": Object { "baseUrl": "", + "href": "", "id": "cloud-deployment-link", - "legacy": false, "title": "Manage cloud deployment", }, "closed": false, @@ -362,7 +362,6 @@ exports[`Header renders 1`] = ` } kibanaDocLink="/docs" kibanaVersion="1.0.0" - legacyMode={false} loadingCount$={ BehaviorSubject { "_isScalar": false, @@ -440,8 +439,8 @@ exports[`Header renders 1`] = ` "_value": Array [ Object { "baseUrl": "", + "href": "", "id": "kibana", - "legacy": false, "title": "kibana", }, ], @@ -850,8 +849,8 @@ exports[`Header renders 2`] = ` "_isScalar": false, "_value": Object { "baseUrl": "", + "href": "", "id": "cloud-deployment-link", - "legacy": false, "title": "Manage cloud deployment", }, "closed": false, @@ -1889,7 +1888,6 @@ exports[`Header renders 2`] = ` } kibanaDocLink="/docs" kibanaVersion="1.0.0" - legacyMode={false} loadingCount$={ BehaviorSubject { "_isScalar": false, @@ -2043,8 +2041,8 @@ exports[`Header renders 2`] = ` "_value": Array [ Object { "baseUrl": "", + "href": "", "id": "kibana", - "legacy": false, "title": "kibana", }, ], @@ -2415,8 +2413,8 @@ exports[`Header renders 2`] = ` "_value": Array [ Object { "baseUrl": "", + "href": "", "id": "kibana", - "legacy": false, "title": "kibana", }, ], @@ -4587,8 +4585,8 @@ exports[`Header renders 2`] = ` "_isScalar": false, "_value": Object { "baseUrl": "", + "href": "", "id": "cloud-deployment-link", - "legacy": false, "title": "Manage cloud deployment", }, "closed": false, @@ -4640,15 +4638,14 @@ exports[`Header renders 2`] = ` id="mockId" isLocked={false} isOpen={false} - legacyMode={false} navLinks$={ BehaviorSubject { "_isScalar": false, "_value": Array [ Object { "baseUrl": "", + "href": "", "id": "kibana", - "legacy": false, "title": "kibana", }, ], @@ -5072,8 +5069,8 @@ exports[`Header renders 3`] = ` "_isScalar": false, "_value": Object { "baseUrl": "", + "href": "", "id": "cloud-deployment-link", - "legacy": false, "title": "Manage cloud deployment", }, "closed": false, @@ -6111,7 +6108,6 @@ exports[`Header renders 3`] = ` } kibanaDocLink="/docs" kibanaVersion="1.0.0" - legacyMode={false} loadingCount$={ BehaviorSubject { "_isScalar": false, @@ -6265,8 +6261,8 @@ exports[`Header renders 3`] = ` "_value": Array [ Object { "baseUrl": "", + "href": "", "id": "kibana", - "legacy": false, "title": "kibana", }, ], @@ -6637,8 +6633,8 @@ exports[`Header renders 3`] = ` "_value": Array [ Object { "baseUrl": "", + "href": "", "id": "kibana", - "legacy": false, "title": "kibana", }, ], @@ -8809,8 +8805,8 @@ exports[`Header renders 3`] = ` "_isScalar": false, "_value": Object { "baseUrl": "", + "href": "", "id": "cloud-deployment-link", - "legacy": false, "title": "Manage cloud deployment", }, "closed": false, @@ -8862,15 +8858,14 @@ exports[`Header renders 3`] = ` id="mockId" isLocked={true} isOpen={false} - legacyMode={false} navLinks$={ BehaviorSubject { "_isScalar": false, "_value": Array [ Object { "baseUrl": "", + "href": "", "id": "kibana", - "legacy": false, "title": "kibana", }, ], @@ -9083,7 +9078,7 @@ exports[`Header renders 3`] = ` Array [ Object { "data-test-subj": "collapsibleNavCustomNavLink", - "href": undefined, + "href": "", "icon": undefined, "iconType": undefined, "isActive": false, @@ -9107,6 +9102,7 @@ exports[`Header renders 3`] = ` @@ -14136,6 +14131,7 @@ exports[`Header renders 4`] = ` className="euiNavDrawerGroup__item" data-name="kibana" data-test-subj="navDrawerAppsMenuLink" + href="" icon={ ) { id: title, href: title, baseUrl: '/', - legacy: false, isActive: true, 'data-test-subj': title, }; @@ -62,7 +61,6 @@ function mockProps() { isLocked: false, isOpen: false, homeHref: '/', - legacyMode: false, navLinks$: new BehaviorSubject([]), recentlyAccessed$: new BehaviorSubject([]), storage: new StubBrowserStorage(), diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 5abd14312f4a6..a5f42c0949562 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -81,7 +81,6 @@ interface Props { isLocked: boolean; isOpen: boolean; homeHref: string; - legacyMode: boolean; navLinks$: Rx.Observable; recentlyAccessed$: Rx.Observable; storage?: Storage; @@ -97,7 +96,6 @@ export function CollapsibleNav({ isLocked, isOpen, homeHref, - legacyMode, storage = window.localStorage, onIsLockedUpdate, closeNav, @@ -116,7 +114,6 @@ export function CollapsibleNav({ const readyForEUI = (link: ChromeNavLink, needsIcon: boolean = false) => { return createEuiListItem({ link, - legacyMode, appId, dataTestSubj: 'collapsibleNavAppLink', navigateToApp, @@ -148,7 +145,6 @@ export function CollapsibleNav({ listItems={[ createEuiListItem({ link: customNavLink, - legacyMode, basePath, navigateToApp, dataTestSubj: 'collapsibleNavCustomNavLink', diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index a9fa15d43182b..04eb256f30f37 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -50,7 +50,6 @@ function mockProps() { forceAppSwitcherNavigation$: new BehaviorSubject(false), helpExtension$: new BehaviorSubject(undefined), helpSupportUrl$: new BehaviorSubject(''), - legacyMode: false, navControlsLeft$: new BehaviorSubject([]), navControlsRight$: new BehaviorSubject([]), basePath: http.basePath, @@ -74,13 +73,13 @@ describe('Header', () => { const isLocked$ = new BehaviorSubject(false); const navType$ = new BehaviorSubject('modern' as NavType); const navLinks$ = new BehaviorSubject([ - { id: 'kibana', title: 'kibana', baseUrl: '', legacy: false }, + { id: 'kibana', title: 'kibana', baseUrl: '', href: '' }, ]); const customNavLink$ = new BehaviorSubject({ id: 'cloud-deployment-link', title: 'Manage cloud deployment', baseUrl: '', - legacy: false, + href: '', }); const recentlyAccessed$ = new BehaviorSubject([ { link: '', label: 'dashboard', id: 'dashboard' }, diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 0624d66a9598b..c0b3fc72930dc 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -67,7 +67,6 @@ export interface HeaderProps { forceAppSwitcherNavigation$: Observable; helpExtension$: Observable; helpSupportUrl$: Observable; - legacyMode: boolean; navControlsLeft$: Observable; navControlsRight$: Observable; basePath: HttpStart['basePath']; @@ -93,7 +92,6 @@ function renderMenuTrigger(toggleOpen: () => void) { export function Header({ kibanaVersion, kibanaDocLink, - legacyMode, application, basePath, onIsLockedUpdate, @@ -195,7 +193,6 @@ export function Header({ isOpen={isOpen} homeHref={homeHref} basePath={basePath} - legacyMode={legacyMode} navigateToApp={application.navigateToApp} onIsLockedUpdate={onIsLockedUpdate} closeNav={() => { @@ -218,7 +215,6 @@ export function Header({ appId$={application.currentAppId$} navigateToApp={application.navigateToApp} ref={navDrawerRef} - legacyMode={legacyMode} /> )} diff --git a/src/core/public/chrome/ui/header/nav_drawer.tsx b/src/core/public/chrome/ui/header/nav_drawer.tsx index ee4bff6cc0ac4..fc080fbafc303 100644 --- a/src/core/public/chrome/ui/header/nav_drawer.tsx +++ b/src/core/public/chrome/ui/header/nav_drawer.tsx @@ -33,7 +33,6 @@ export interface Props { appId$: InternalApplicationStart['currentAppId$']; basePath: HttpStart['basePath']; isLocked?: boolean; - legacyMode: boolean; navLinks$: Observable; recentlyAccessed$: Observable; navigateToApp: CoreStart['application']['navigateToApp']; @@ -41,7 +40,7 @@ export interface Props { } function NavDrawerRenderer( - { isLocked, onIsLockedUpdate, basePath, legacyMode, navigateToApp, ...observables }: Props, + { isLocked, onIsLockedUpdate, basePath, navigateToApp, ...observables }: Props, ref: React.Ref ) { const appId = useObservable(observables.appId$, ''); @@ -67,7 +66,6 @@ function NavDrawerRenderer( listItems={navLinks.map((link) => createEuiListItem({ link, - legacyMode, appId, basePath, navigateToApp, diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index c70a40f49643e..04d9c5bf7a10a 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -29,7 +29,6 @@ export const isModifiedOrPrevented = (event: React.MouseEvent {}, @@ -52,12 +50,7 @@ export function createEuiListItem({ dataTestSubj, externalLink = false, }: Props) { - const { legacy, active, id, title, disabled, euiIconType, icon, tooltip } = link; - let { href } = link; - - if (legacy) { - href = link.url && !active ? link.url : link.baseUrl; - } + const { href, id, title, disabled, euiIconType, icon, tooltip } = link; return { label: tooltip ?? title, @@ -70,8 +63,6 @@ export function createEuiListItem({ if ( !externalLink && // ignore external links - !legacyMode && // ignore when in legacy mode - !legacy && // ignore links to legacy apps event.button === 0 && // ignore everything but left clicks !isModifiedOrPrevented(event) ) { @@ -79,8 +70,7 @@ export function createEuiListItem({ navigateToApp(id); } }, - // Legacy apps use `active` property, NP apps should match the current app - isActive: active || appId === id, + isActive: appId === id, isDisabled: disabled, 'data-test-subj': dataTestSubj, ...(basePath && { @@ -116,7 +106,7 @@ export function createRecentNavLink( ) { const { link, label } = recentLink; const href = relativeToAbsolute(basePath.prepend(link)); - const navLink = navLinks.find((nl) => href.startsWith(nl.baseUrl ?? nl.subUrlBase)); + const navLink = navLinks.find((nl) => href.startsWith(nl.baseUrl)); let titleAndAriaLabel = label; if (navLink) { diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index b5b99418b44b4..d0e457386ffca 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -23,7 +23,6 @@ import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock import { httpServiceMock } from './http/http_service.mock'; import { i18nServiceMock } from './i18n/i18n_service.mock'; import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; -import { legacyPlatformServiceMock } from './legacy/legacy_service.mock'; import { notificationServiceMock } from './notifications/notifications_service.mock'; import { overlayServiceMock } from './overlays/overlay_service.mock'; import { pluginsServiceMock } from './plugins/plugins_service.mock'; @@ -34,14 +33,6 @@ import { contextServiceMock } from './context/context_service.mock'; import { integrationsServiceMock } from './integrations/integrations_service.mock'; import { coreAppMock } from './core_app/core_app.mock'; -export const MockLegacyPlatformService = legacyPlatformServiceMock.create(); -export const LegacyPlatformServiceConstructor = jest - .fn() - .mockImplementation(() => MockLegacyPlatformService); -jest.doMock('./legacy', () => ({ - LegacyPlatformService: LegacyPlatformServiceConstructor, -})); - export const MockInjectedMetadataService = injectedMetadataServiceMock.create(); export const InjectedMetadataServiceConstructor = jest .fn() diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 4c1993c90a2e1..213237309c30b 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -23,13 +23,11 @@ import { HttpServiceConstructor, I18nServiceConstructor, InjectedMetadataServiceConstructor, - LegacyPlatformServiceConstructor, MockChromeService, MockFatalErrorsService, MockHttpService, MockI18nService, MockInjectedMetadataService, - MockLegacyPlatformService, MockNotificationsService, MockOverlayService, MockPluginsService, @@ -80,7 +78,6 @@ describe('constructor', () => { createCoreSystem(); expect(InjectedMetadataServiceConstructor).toHaveBeenCalledTimes(1); - expect(LegacyPlatformServiceConstructor).toHaveBeenCalledTimes(1); expect(I18nServiceConstructor).toHaveBeenCalledTimes(1); expect(FatalErrorsServiceConstructor).toHaveBeenCalledTimes(1); expect(NotificationServiceConstructor).toHaveBeenCalledTimes(1); @@ -106,25 +103,6 @@ describe('constructor', () => { }); }); - it('passes required params to LegacyPlatformService', () => { - const requireLegacyFiles = { requireLegacyFiles: true }; - const requireLegacyBootstrapModule = { requireLegacyBootstrapModule: true }; - const requireNewPlatformShimModule = { requireNewPlatformShimModule: true }; - - createCoreSystem({ - requireLegacyFiles, - requireLegacyBootstrapModule, - requireNewPlatformShimModule, - }); - - expect(LegacyPlatformServiceConstructor).toHaveBeenCalledTimes(1); - expect(LegacyPlatformServiceConstructor).toHaveBeenCalledWith({ - requireLegacyFiles, - requireLegacyBootstrapModule, - requireNewPlatformShimModule, - }); - }); - it('passes browserSupportsCsp to ChromeService', () => { createCoreSystem(); @@ -190,7 +168,6 @@ describe('#setup()', () => { pluginDependencies: new Map([ [pluginA, []], [pluginB, [pluginA]], - [MockLegacyPlatformService.legacyId, [pluginA, pluginB]], ]), }); }); @@ -301,11 +278,6 @@ describe('#start()', () => { expect(MockPluginsService.start).toHaveBeenCalledTimes(1); }); - it('calls legacyPlatform#start()', async () => { - await startCore(); - expect(MockLegacyPlatformService.start).toHaveBeenCalledTimes(1); - }); - it('calls overlays#start()', async () => { await startCore(); expect(MockOverlayService.start).toHaveBeenCalledTimes(1); @@ -317,7 +289,6 @@ describe('#start()', () => { expect(MockRenderingService.start).toHaveBeenCalledWith({ application: expect.any(Object), chrome: expect.any(Object), - injectedMetadata: expect.any(Object), overlays: expect.any(Object), targetDomElement: expect.any(HTMLElement), }); @@ -335,14 +306,6 @@ describe('#start()', () => { }); describe('#stop()', () => { - it('calls legacyPlatform.stop()', () => { - const coreSystem = createCoreSystem(); - - expect(MockLegacyPlatformService.stop).not.toHaveBeenCalled(); - coreSystem.stop(); - expect(MockLegacyPlatformService.stop).toHaveBeenCalled(); - }); - it('calls notifications.stop()', () => { const coreSystem = createCoreSystem(); @@ -422,7 +385,6 @@ describe('RenderingService targetDomElement', () => { let targetDomElementParentInStart: HTMLElement | null; MockRenderingService.start.mockImplementation(({ targetDomElement }) => { targetDomElementParentInStart = targetDomElement.parentElement; - return { legacyTargetDomElement: document.createElement('div') }; }); // Starting the core system should pass the targetDomElement as a child of the rootDomElement @@ -432,24 +394,6 @@ describe('RenderingService targetDomElement', () => { }); }); -describe('LegacyPlatformService targetDomElement', () => { - it('only mounts the element when start, after setting up the legacyPlatformService', async () => { - const core = createCoreSystem(); - - let targetDomElementInStart: HTMLElement | undefined; - MockLegacyPlatformService.start.mockImplementation(({ targetDomElement }) => { - targetDomElementInStart = targetDomElement; - }); - - await core.setup(); - await core.start(); - // Starting the core system should pass the legacyTargetDomElement to the LegacyPlatformService - const renderingLegacyTargetDomElement = - MockRenderingService.start.mock.results[0].value.legacyTargetDomElement; - expect(targetDomElementInStart!).toBe(renderingLegacyTargetDomElement); - }); -}); - describe('Notifications targetDomElement', () => { it('only mounts the element when started, after setting up the notificationsService', async () => { const rootDomElement = document.createElement('div'); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index e08841b0271d9..006d0036f7a12 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -30,13 +30,12 @@ import { InjectedMetadataSetup, InjectedMetadataStart, } from './injected_metadata'; -import { LegacyPlatformParams, LegacyPlatformService } from './legacy'; import { NotificationsService } from './notifications'; import { OverlayService } from './overlays'; import { PluginsService } from './plugins'; import { UiSettingsService } from './ui_settings'; import { ApplicationService } from './application'; -import { mapToObject, pick } from '../utils/'; +import { pick } from '../utils/'; import { DocLinksService } from './doc_links'; import { RenderingService } from './rendering'; import { SavedObjectsService } from './saved_objects'; @@ -49,9 +48,6 @@ interface Params { rootDomElement: HTMLElement; browserSupportsCsp: boolean; injectedMetadata: InjectedMetadataParams['injectedMetadata']; - requireLegacyFiles?: LegacyPlatformParams['requireLegacyFiles']; - requireLegacyBootstrapModule?: LegacyPlatformParams['requireLegacyBootstrapModule']; - requireNewPlatformShimModule?: LegacyPlatformParams['requireNewPlatformShimModule']; } /** @internal */ @@ -86,7 +82,6 @@ export interface InternalCoreStart extends Omit { export class CoreSystem { private readonly fatalErrors: FatalErrorsService; private readonly injectedMetadata: InjectedMetadataService; - private readonly legacy: LegacyPlatformService; private readonly notifications: NotificationsService; private readonly http: HttpService; private readonly savedObjects: SavedObjectsService; @@ -107,14 +102,7 @@ export class CoreSystem { private fatalErrorsSetup: FatalErrorsSetup | null = null; constructor(params: Params) { - const { - rootDomElement, - browserSupportsCsp, - injectedMetadata, - requireLegacyFiles, - requireLegacyBootstrapModule, - requireNewPlatformShimModule, - } = params; + const { rootDomElement, browserSupportsCsp, injectedMetadata } = params; this.rootDomElement = rootDomElement; @@ -145,12 +133,6 @@ export class CoreSystem { this.context = new ContextService(this.coreContext); this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.coreApp = new CoreApp(this.coreContext); - - this.legacy = new LegacyPlatformService({ - requireLegacyFiles, - requireLegacyBootstrapModule, - requireNewPlatformShimModule, - }); } public async setup() { @@ -170,16 +152,9 @@ export class CoreSystem { const pluginDependencies = this.plugins.getOpaqueIds(); const context = this.context.setup({ - // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: - // 1) Can access context from any NP plugin - // 2) Can register context providers that will only be available to other legacy plugins and will not leak into - // New Platform plugins. - pluginDependencies: new Map([ - ...pluginDependencies, - [this.legacy.legacyId, [...pluginDependencies.keys()]], - ]), + pluginDependencies: new Map([...pluginDependencies]), }); - const application = this.application.setup({ context, http, injectedMetadata }); + const application = this.application.setup({ context, http }); this.coreApp.setup({ application, http, injectedMetadata, notifications }); const core: InternalCoreSetup = { @@ -193,12 +168,7 @@ export class CoreSystem { }; // Services that do not expose contracts at setup - const plugins = await this.plugins.setup(core); - - await this.legacy.setup({ - core, - plugins: mapToObject(plugins.contracts), - }); + await this.plugins.setup(core); return { fatalErrors: this.fatalErrorsSetup }; } catch (error) { @@ -277,7 +247,7 @@ export class CoreSystem { fatalErrors, }; - const plugins = await this.plugins.start(core); + await this.plugins.start(core); // ensure the rootDomElement is empty this.rootDomElement.textContent = ''; @@ -286,20 +256,13 @@ export class CoreSystem { this.rootDomElement.appendChild(notificationsTargetDomElement); this.rootDomElement.appendChild(overlayTargetDomElement); - const rendering = this.rendering.start({ + this.rendering.start({ application, chrome, - injectedMetadata, overlays, targetDomElement: coreUiTargetDomElement, }); - await this.legacy.start({ - core, - plugins: mapToObject(plugins.contracts), - targetDomElement: rendering.legacyTargetDomElement, - }); - return { application, }; @@ -315,7 +278,6 @@ export class CoreSystem { } public stop() { - this.legacy.stop(); this.plugins.stop(); this.coreApp.stop(); this.notifications.stop(); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 9176a277b3f43..a9774dafd2340 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -61,7 +61,6 @@ import { import { FatalErrorsSetup, FatalErrorsStart, FatalErrorInfo } from './fatal_errors'; import { HttpSetup, HttpStart } from './http'; import { I18nStart } from './i18n'; -import { InjectedMetadataSetup, InjectedMetadataStart, LegacyNavLink } from './injected_metadata'; import { NotificationsSetup, NotificationsStart } from './notifications'; import { OverlayStart } from './overlays'; import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins'; @@ -106,7 +105,6 @@ export { ApplicationStart, App, PublicAppInfo, - AppBase, AppMount, AppMountDeprecated, AppUnmount, @@ -122,8 +120,6 @@ export { AppUpdatableFields, AppUpdater, ScopedHistory, - LegacyApp, - PublicLegacyAppInfo, NavigateToAppOptions, } from './application'; @@ -230,7 +226,7 @@ export interface CoreSetup { - /** @deprecated */ - injectedMetadata: InjectedMetadataSetup; -} - -/** - * Start interface exposed to the legacy platform via the `ui/new_platform` module. - * - * @remarks - * Some methods are not supported in the legacy platform and while present to make this type compatibile with - * {@link CoreStart}, unsupported methods will throw exceptions when called. - * - * @public - * @deprecated - */ -export interface LegacyCoreStart extends CoreStart { - /** @deprecated */ - injectedMetadata: InjectedMetadataStart; -} - export { Capabilities, ChromeBadge, @@ -356,7 +322,6 @@ export { HttpSetup, HttpStart, I18nStart, - LegacyNavLink, NotificationsSetup, NotificationsStart, Plugin, diff --git a/src/core/public/injected_metadata/index.ts b/src/core/public/injected_metadata/index.ts index cebd0f017de69..925eeab187535 100644 --- a/src/core/public/injected_metadata/index.ts +++ b/src/core/public/injected_metadata/index.ts @@ -23,5 +23,4 @@ export { InjectedMetadataSetup, InjectedMetadataStart, InjectedPluginMetadata, - LegacyNavLink, } from './injected_metadata_service'; diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index e6b1c440519bd..3bb4358406246 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -25,7 +25,6 @@ const createSetupContractMock = () => { getKibanaVersion: jest.fn(), getKibanaBranch: jest.fn(), getCspConfig: jest.fn(), - getLegacyMode: jest.fn(), getAnonymousStatusPage: jest.fn(), getLegacyMetadata: jest.fn(), getPlugins: jest.fn(), @@ -35,7 +34,6 @@ const createSetupContractMock = () => { }; setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); setupContract.getKibanaVersion.mockReturnValue('kibanaVersion'); - setupContract.getLegacyMode.mockReturnValue(true); setupContract.getAnonymousStatusPage.mockReturnValue(false); setupContract.getLegacyMetadata.mockReturnValue({ app: { diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index db4bfdf415bcc..23630a5bcf228 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -28,17 +28,6 @@ import { import { deepFreeze } from '../../utils/'; import { AppCategory } from '../'; -/** @public */ -export interface LegacyNavLink { - id: string; - category?: AppCategory; - title: string; - order: number; - url: string; - icon?: string; - euiIconType?: string; -} - export interface InjectedPluginMetadata { id: PluginName; plugin: DiscoveredPlugin; @@ -67,7 +56,6 @@ export interface InjectedMetadataParams { packageInfo: Readonly; }; uiPlugins: InjectedPluginMetadata[]; - legacyMode: boolean; anonymousStatusPage: boolean; legacyMetadata: { app: { @@ -75,7 +63,6 @@ export interface InjectedMetadataParams { title: string; }; bundleId: string; - nav: LegacyNavLink[]; version: string; branch: string; buildNum: number; @@ -137,10 +124,6 @@ export class InjectedMetadataService { return this.state.uiPlugins; }, - getLegacyMode: () => { - return this.state.legacyMode; - }, - getLegacyMetadata: () => { return this.state.legacyMetadata; }, @@ -182,8 +165,6 @@ export interface InjectedMetadataSetup { * An array of frontend plugins in topological order. */ getPlugins: () => InjectedPluginMetadata[]; - /** Indicates whether or not we are rendering a known legacy app. */ - getLegacyMode: () => boolean; getAnonymousStatusPage: () => boolean; getLegacyMetadata: () => { app: { @@ -191,7 +172,6 @@ export interface InjectedMetadataSetup { title: string; }; bundleId: string; - nav: LegacyNavLink[]; version: string; branch: string; buildNum: number; diff --git a/src/core/public/legacy/index.ts b/src/core/public/legacy/index.ts deleted file mode 100644 index 1ea43d8deebbc..0000000000000 --- a/src/core/public/legacy/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { LegacyPlatformService, LegacyPlatformParams } from './legacy_service'; diff --git a/src/core/public/legacy/legacy_service.mock.ts b/src/core/public/legacy/legacy_service.mock.ts deleted file mode 100644 index 0c8d9682185d5..0000000000000 --- a/src/core/public/legacy/legacy_service.mock.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { LegacyPlatformService } from './legacy_service'; - -// Use Required to get only public properties -type LegacyPlatformServiceContract = Required; -const createMock = () => { - const mocked: jest.Mocked = { - legacyId: Symbol(), - setup: jest.fn(), - start: jest.fn(), - stop: jest.fn(), - }; - return mocked; -}; - -export const legacyPlatformServiceMock = { - create: createMock, -}; diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts deleted file mode 100644 index cb29abc9b0ccc..0000000000000 --- a/src/core/public/legacy/legacy_service.test.ts +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import angular from 'angular'; - -import { chromeServiceMock } from '../chrome/chrome_service.mock'; -import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; -import { httpServiceMock } from '../http/http_service.mock'; -import { i18nServiceMock } from '../i18n/i18n_service.mock'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; -import { notificationServiceMock } from '../notifications/notifications_service.mock'; -import { overlayServiceMock } from '../overlays/overlay_service.mock'; -import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; -import { LegacyPlatformService } from './legacy_service'; -import { applicationServiceMock } from '../application/application_service.mock'; -import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; -import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; -import { contextServiceMock } from '../context/context_service.mock'; - -const applicationSetup = applicationServiceMock.createInternalSetupContract(); -const contextSetup = contextServiceMock.createSetupContract(); -const docLinksSetup = docLinksServiceMock.createSetupContract(); -const fatalErrorsSetup = fatalErrorsServiceMock.createSetupContract(); -const httpSetup = httpServiceMock.createSetupContract(); -const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract(); -const notificationsSetup = notificationServiceMock.createSetupContract(); -const uiSettingsSetup = uiSettingsServiceMock.createSetupContract(); - -const mockLoadOrder: string[] = []; -const mockUiNewPlatformSetup = jest.fn(); -const mockUiNewPlatformStart = jest.fn(); -const mockUiChromeBootstrap = jest.fn(); -const defaultParams = { - requireLegacyFiles: jest.fn(() => { - mockLoadOrder.push('legacy files'); - }), - requireLegacyBootstrapModule: jest.fn(() => { - mockLoadOrder.push('ui/chrome'); - return { - bootstrap: mockUiChromeBootstrap, - }; - }), - requireNewPlatformShimModule: jest.fn(() => ({ - __setup__: mockUiNewPlatformSetup, - __start__: mockUiNewPlatformStart, - })), -}; - -const defaultSetupDeps = { - core: { - application: applicationSetup, - context: contextSetup, - docLinks: docLinksSetup, - fatalErrors: fatalErrorsSetup, - injectedMetadata: injectedMetadataSetup, - notifications: notificationsSetup, - http: httpSetup, - uiSettings: uiSettingsSetup, - }, - plugins: {}, -}; - -const applicationStart = applicationServiceMock.createInternalStartContract(); -const docLinksStart = docLinksServiceMock.createStartContract(); -const httpStart = httpServiceMock.createStartContract(); -const chromeStart = chromeServiceMock.createStartContract(); -const i18nStart = i18nServiceMock.createStartContract(); -const injectedMetadataStart = injectedMetadataServiceMock.createStartContract(); -const notificationsStart = notificationServiceMock.createStartContract(); -const overlayStart = overlayServiceMock.createStartContract(); -const uiSettingsStart = uiSettingsServiceMock.createStartContract(); -const savedObjectsStart = savedObjectsServiceMock.createStartContract(); -const fatalErrorsStart = fatalErrorsServiceMock.createStartContract(); -const mockStorage = { getItem: jest.fn() } as any; - -const defaultStartDeps = { - core: { - application: applicationStart, - docLinks: docLinksStart, - http: httpStart, - chrome: chromeStart, - i18n: i18nStart, - injectedMetadata: injectedMetadataStart, - notifications: notificationsStart, - overlays: overlayStart, - uiSettings: uiSettingsStart, - savedObjects: savedObjectsStart, - fatalErrors: fatalErrorsStart, - }, - lastSubUrlStorage: mockStorage, - targetDomElement: document.createElement('div'), - plugins: {}, -}; - -afterEach(() => { - jest.clearAllMocks(); - jest.resetModules(); - mockLoadOrder.length = 0; -}); - -describe('#setup()', () => { - describe('default', () => { - it('initializes new platform shim module with core APIs', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - - expect(mockUiNewPlatformSetup).toHaveBeenCalledTimes(1); - expect(mockUiNewPlatformSetup).toHaveBeenCalledWith(expect.any(Object), {}); - }); - - it('throws error if requireNewPlatformShimModule is undefined', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - requireNewPlatformShimModule: undefined, - }); - - expect(() => { - legacyPlatform.setup(defaultSetupDeps); - }).toThrowErrorMatchingInlineSnapshot( - `"requireNewPlatformShimModule must be specified when rendering a legacy application"` - ); - - expect(mockUiNewPlatformSetup).not.toHaveBeenCalled(); - }); - }); -}); - -describe('#start()', () => { - it('fetches and sets legacy lastSubUrls', () => { - chromeStart.navLinks.getAll.mockReturnValue([ - { id: 'link1', baseUrl: 'http://wowza.com/app1', legacy: true } as any, - ]); - mockStorage.getItem.mockReturnValue('http://wowza.com/app1/subUrl'); - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start({ ...defaultStartDeps, lastSubUrlStorage: mockStorage }); - - expect(chromeStart.navLinks.update).toHaveBeenCalledWith('link1', { - url: 'http://wowza.com/app1/subUrl', - }); - }); - - it('initializes ui/new_platform with core APIs', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); - - expect(mockUiNewPlatformStart).toHaveBeenCalledTimes(1); - expect(mockUiNewPlatformStart).toHaveBeenCalledWith(expect.any(Object), {}); - }); - - it('throws error if requireNewPlatformShimeModule is undefined', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - requireNewPlatformShimModule: undefined, - }); - - expect(() => { - legacyPlatform.start(defaultStartDeps); - }).toThrowErrorMatchingInlineSnapshot( - `"requireNewPlatformShimModule must be specified when rendering a legacy application"` - ); - - expect(mockUiNewPlatformStart).not.toHaveBeenCalled(); - }); - - it('resolves getStartServices with core and plugin APIs', async () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); - - const { getStartServices } = mockUiNewPlatformSetup.mock.calls[0][0]; - const [coreStart, pluginsStart] = await getStartServices(); - expect(coreStart).toEqual(expect.any(Object)); - expect(pluginsStart).toBe(defaultStartDeps.plugins); - }); - - it('passes the targetDomElement to legacy bootstrap module', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); - - expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1); - expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement); - }); - - describe('load order', () => { - it('loads ui/modules before ui/chrome, and both before legacy files', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - expect(mockLoadOrder).toEqual([]); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); - - expect(mockLoadOrder).toMatchInlineSnapshot(` - Array [ - "ui/chrome", - "legacy files", - ] - `); - }); - }); -}); - -describe('#stop()', () => { - it('does nothing if angular was not bootstrapped to targetDomElement', () => { - const targetDomElement = document.createElement('div'); - targetDomElement.innerHTML = ` -

this should not be removed

- `; - - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - legacyPlatform.stop(); - expect(targetDomElement).toMatchInlineSnapshot(` -
- - -

- this should not be removed -

- - -
- `); - }); - - it('destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement', async () => { - const targetDomElement = document.createElement('div'); - const scopeDestroySpy = jest.fn(); - - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - // simulate bootstrapping with a module "foo" - angular.module('foo', []).directive('bar', () => ({ - restrict: 'E', - link($scope) { - $scope.$on('$destroy', scopeDestroySpy); - }, - })); - - targetDomElement.innerHTML = ` - - `; - - angular.bootstrap(targetDomElement, ['foo']); - - await legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start({ ...defaultStartDeps, targetDomElement }); - legacyPlatform.stop(); - - expect(targetDomElement).toMatchInlineSnapshot(` -
- `); - expect(scopeDestroySpy).toHaveBeenCalledTimes(1); - }); -}); diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts deleted file mode 100644 index 78a9219f3d694..0000000000000 --- a/src/core/public/legacy/legacy_service.ts +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import angular from 'angular'; -import { first } from 'rxjs/operators'; -import { Subject } from 'rxjs'; -import { InternalCoreSetup, InternalCoreStart } from '../core_system'; -import { LegacyCoreSetup, LegacyCoreStart, MountPoint } from '../'; - -/** @internal */ -export interface LegacyPlatformParams { - requireLegacyFiles?: () => void; - requireLegacyBootstrapModule?: () => BootstrapModule; - requireNewPlatformShimModule?: () => { - __setup__: (legacyCore: LegacyCoreSetup, plugins: Record) => void; - __start__: (legacyCore: LegacyCoreStart, plugins: Record) => void; - }; -} - -interface SetupDeps { - core: InternalCoreSetup; - plugins: Record; -} - -interface StartDeps { - core: InternalCoreStart; - plugins: Record; - lastSubUrlStorage?: Storage; - targetDomElement?: HTMLElement; -} - -interface BootstrapModule { - bootstrap: MountPoint; -} - -/** - * The LegacyPlatformService is responsible for initializing - * the legacy platform by injecting parts of the new platform - * services into the legacy platform modules, like ui/modules, - * and then bootstrapping the ui/chrome or ~~ui/test_harness~~ to - * setup either the app or browser tests. - */ -export class LegacyPlatformService { - /** Symbol to represent the legacy platform as a fake "plugin". Used by the ContextService */ - public readonly legacyId = Symbol(); - private bootstrapModule?: BootstrapModule; - private targetDomElement?: HTMLElement; - private readonly startDependencies$ = new Subject<[LegacyCoreStart, object, {}]>(); - private readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); - - constructor(private readonly params: LegacyPlatformParams) {} - - public setup({ core, plugins }: SetupDeps) { - // Always register legacy apps, even if not in legacy mode. - core.injectedMetadata.getLegacyMetadata().nav.forEach((navLink: any) => - core.application.registerLegacyApp({ - id: navLink.id, - order: navLink.order, - title: navLink.title, - euiIconType: navLink.euiIconType, - icon: navLink.icon, - appUrl: navLink.url, - subUrlBase: navLink.subUrlBase, - linkToLastSubUrl: navLink.linkToLastSubUrl, - category: navLink.category, - disableSubUrlTracking: navLink.disableSubUrlTracking, - }) - ); - - const legacyCore: LegacyCoreSetup = { - ...core, - getStartServices: () => this.startDependencies, - application: { - ...core.application, - register: notSupported(`core.application.register()`), - registerMountContext: notSupported(`core.application.registerMountContext()`), - }, - }; - - // Inject parts of the new platform into parts of the legacy platform - // so that legacy APIs/modules can mimic their new platform counterparts - if (core.injectedMetadata.getLegacyMode()) { - if (!this.params.requireNewPlatformShimModule) { - throw new Error( - `requireNewPlatformShimModule must be specified when rendering a legacy application` - ); - } - - this.params.requireNewPlatformShimModule().__setup__(legacyCore, plugins); - } - } - - public start({ - core, - targetDomElement, - plugins, - lastSubUrlStorage = window.sessionStorage, - }: StartDeps) { - // Initialize legacy sub urls - core.chrome.navLinks - .getAll() - .filter((link) => link.legacy) - .forEach((navLink) => { - const lastSubUrl = lastSubUrlStorage.getItem(`lastSubUrl:${navLink.baseUrl}`); - core.chrome.navLinks.update(navLink.id, { - url: lastSubUrl || navLink.url || navLink.baseUrl, - }); - }); - - // Only import and bootstrap legacy platform if we're in legacy mode. - if (!core.injectedMetadata.getLegacyMode()) { - return; - } - - const legacyCore: LegacyCoreStart = { - ...core, - application: { - applications$: core.application.applications$, - currentAppId$: core.application.currentAppId$, - capabilities: core.application.capabilities, - getUrlForApp: core.application.getUrlForApp, - navigateToApp: core.application.navigateToApp, - navigateToUrl: core.application.navigateToUrl, - registerMountContext: notSupported(`core.application.registerMountContext()`), - }, - }; - - this.startDependencies$.next([legacyCore, plugins, {}]); - - if (!this.params.requireNewPlatformShimModule) { - throw new Error( - `requireNewPlatformShimModule must be specified when rendering a legacy application` - ); - } - if (!this.params.requireLegacyBootstrapModule) { - throw new Error( - `requireLegacyBootstrapModule must be specified when rendering a legacy application` - ); - } - - // Inject parts of the new platform into parts of the legacy platform - // so that legacy APIs/modules can mimic their new platform counterparts - this.params.requireNewPlatformShimModule().__start__(legacyCore, plugins); - - // Load the bootstrap module before loading the legacy platform files so that - // the bootstrap module can modify the environment a bit first - this.bootstrapModule = this.params.requireLegacyBootstrapModule(); - - // require the files that will tie into the legacy platform - if (this.params.requireLegacyFiles) { - this.params.requireLegacyFiles(); - } - - if (!this.bootstrapModule) { - throw new Error('Bootstrap module must be loaded before `start`'); - } - - this.targetDomElement = targetDomElement; - - // `targetDomElement` is always defined when in legacy mode - this.bootstrapModule.bootstrap(this.targetDomElement!); - } - - public stop() { - if (!this.targetDomElement) { - return; - } - - const angularRoot = angular.element(this.targetDomElement); - const injector$ = angularRoot.injector(); - - // if we haven't gotten to the point of bootstrapping - // angular, injector$ won't be defined - if (!injector$) { - return; - } - - // destroy the root angular scope - injector$.get('$rootScope').$destroy(); - - // clear the inner html of the root angular element - this.targetDomElement.textContent = ''; - } -} - -const notSupported = (methodName: string) => (...args: any[]) => { - throw new Error(`${methodName} is not supported in the legacy platform.`); -}; diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index aefcb830d40bf..8ed415c09806c 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -42,7 +42,6 @@ export { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock export { httpServiceMock } from './http/http_service.mock'; export { i18nServiceMock } from './i18n/i18n_service.mock'; export { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; -export { legacyPlatformServiceMock } from './legacy/legacy_service.mock'; export { notificationServiceMock } from './notifications/notifications_service.mock'; export { overlayServiceMock } from './overlays/overlay_service.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index bacbd6e757114..c473ea67d9bcd 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -7,6 +7,7 @@ import { Action } from 'history'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; import Boom from 'boom'; +import { ErrorToastOptions as ErrorToastOptions_2 } from 'src/core/public/notifications'; import { EuiBreadcrumb } from '@elastic/eui'; import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; @@ -27,7 +28,9 @@ import { PublicUiSettingsParams as PublicUiSettingsParams_2 } from 'src/core/ser import React from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; import * as Rx from 'rxjs'; +import { SavedObject as SavedObject_2 } from 'src/core/server'; import { ShallowPromise } from '@kbn/utility-types'; +import { ToastInputFields as ToastInputFields_2 } from 'src/core/public/notifications'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; @@ -39,25 +42,18 @@ import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/type // @internal (undocumented) export function __kbnBootstrap__(): void; -// @public -export interface App extends AppBase { - appRoute?: string; - chromeless?: boolean; - exactRoute?: boolean; - mount: AppMount | AppMountDeprecated; -} - // @public (undocumented) -export interface AppBase { +export interface App { + appRoute?: string; capabilities?: Partial; category?: AppCategory; chromeless?: boolean; defaultPath?: string; euiIconType?: string; + exactRoute?: boolean; icon?: string; id: string; - // @internal - legacy?: boolean; + mount: AppMount | AppMountDeprecated; navLinkStatus?: AppNavLinkStatus; order?: number; status?: AppStatus; @@ -121,7 +117,7 @@ export interface ApplicationSetup { // @public (undocumented) export interface ApplicationStart { - applications$: Observable>; + applications$: Observable>; capabilities: RecursiveReadonly; currentAppId$: Observable; getUrlForApp(appId: string, options?: { @@ -186,10 +182,10 @@ export enum AppStatus { export type AppUnmount = () => void; // @public -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick; // @public -export type AppUpdater = (app: AppBase) => Partial | undefined; +export type AppUpdater = (app: App) => Partial | undefined; // @public export function assertNever(x: never): never; @@ -227,10 +223,6 @@ export type ChromeBreadcrumb = EuiBreadcrumb; // @public export interface ChromeDocTitle { - // @internal (undocumented) - __legacy: { - setBaseTitle(baseTitle: string): void; - }; change(newTitle: string | string[]): void; reset(): void; } @@ -290,28 +282,18 @@ export interface ChromeNavControls { // @public (undocumented) export interface ChromeNavLink { - // @deprecated - readonly active?: boolean; readonly baseUrl: string; readonly category?: AppCategory; - // @deprecated readonly disabled?: boolean; - // @deprecated - readonly disableSubUrlTracking?: boolean; readonly euiIconType?: string; readonly hidden?: boolean; - readonly href?: string; + readonly href: string; readonly icon?: string; readonly id: string; - // @internal - readonly legacy: boolean; - // @deprecated - readonly linkToLastSubUrl?: boolean; readonly order?: number; - // @deprecated - readonly subUrlBase?: string; readonly title: string; readonly tooltip?: string; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AppBase" readonly url?: string; } @@ -324,12 +306,14 @@ export interface ChromeNavLinks { getNavLinks$(): Observable>>; has(id: string): boolean; showOnly(id: string): void; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AppBase" + // // @deprecated update(id: string, values: ChromeNavLinkUpdateableFields): ChromeNavLink | undefined; } // @public (undocumented) -export type ChromeNavLinkUpdateableFields = Partial>; +export type ChromeNavLinkUpdateableFields = Partial>; // @public export interface ChromeRecentlyAccessed { @@ -881,52 +865,6 @@ export interface IUiSettingsClient { set: (key: string, value: any) => Promise; } -// @public (undocumented) -export interface LegacyApp extends AppBase { - // (undocumented) - appUrl: string; - // (undocumented) - disableSubUrlTracking?: boolean; - // (undocumented) - linkToLastSubUrl?: boolean; - // (undocumented) - subUrlBase?: string; -} - -// @public @deprecated -export interface LegacyCoreSetup extends CoreSetup { - // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts - // - // @deprecated (undocumented) - injectedMetadata: InjectedMetadataSetup; -} - -// @public @deprecated -export interface LegacyCoreStart extends CoreStart { - // Warning: (ae-forgotten-export) The symbol "InjectedMetadataStart" needs to be exported by the entry point index.d.ts - // - // @deprecated (undocumented) - injectedMetadata: InjectedMetadataStart; -} - -// @public (undocumented) -export interface LegacyNavLink { - // (undocumented) - category?: AppCategory; - // (undocumented) - euiIconType?: string; - // (undocumented) - icon?: string; - // (undocumented) - id: string; - // (undocumented) - order: number; - // (undocumented) - title: string; - // (undocumented) - url: string; -} - // @public export function modifyUrl(url: string, urlModifier: (urlParts: URLMeaningfulParts) => Partial | void): string; @@ -1040,19 +978,11 @@ export type PluginOpaqueId = symbol; // @public export type PublicAppInfo = Omit & { - legacy: false; status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; }; -// @public -export type PublicLegacyAppInfo = Omit & { - legacy: true; - status: AppStatus; - navLinkStatus: AppNavLinkStatus; -}; - // @public export type PublicUiSettingsParams = Omit; @@ -1545,6 +1475,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:215:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:185:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/public/rendering/index.ts b/src/core/public/rendering/index.ts index 7c1ea7031b763..1de82a50a36b5 100644 --- a/src/core/public/rendering/index.ts +++ b/src/core/public/rendering/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { RenderingService, RenderingStart } from './rendering_service'; +export { RenderingService } from './rendering_service'; diff --git a/src/core/public/rendering/rendering_service.mock.ts b/src/core/public/rendering/rendering_service.mock.ts index bb4e7cb49f150..bb4723e69ab5e 100644 --- a/src/core/public/rendering/rendering_service.mock.ts +++ b/src/core/public/rendering/rendering_service.mock.ts @@ -17,25 +17,16 @@ * under the License. */ -import { RenderingStart, RenderingService } from './rendering_service'; - -const createStartContractMock = () => { - const setupContract: jest.Mocked = { - legacyTargetDomElement: document.createElement('div'), - }; - return setupContract; -}; +import { RenderingService } from './rendering_service'; type RenderingServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { start: jest.fn(), }; - mocked.start.mockReturnValue(createStartContractMock()); return mocked; }; export const renderingServiceMock = { create: createMock, - createStartContract: createStartContractMock, }; diff --git a/src/core/public/rendering/rendering_service.test.tsx b/src/core/public/rendering/rendering_service.test.tsx index 437a602a3d447..37658cb51c46f 100644 --- a/src/core/public/rendering/rendering_service.test.tsx +++ b/src/core/public/rendering/rendering_service.test.tsx @@ -23,7 +23,6 @@ import { act } from 'react-dom/test-utils'; import { RenderingService } from './rendering_service'; import { applicationServiceMock } from '../application/application_service.mock'; import { chromeServiceMock } from '../chrome/chrome_service.mock'; -import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { overlayServiceMock } from '../overlays/overlay_service.mock'; import { BehaviorSubject } from 'rxjs'; @@ -31,7 +30,6 @@ describe('RenderingService#start', () => { let application: ReturnType; let chrome: ReturnType; let overlays: ReturnType; - let injectedMetadata: ReturnType; let targetDomElement: HTMLDivElement; let rendering: RenderingService; @@ -45,8 +43,6 @@ describe('RenderingService#start', () => { overlays = overlayServiceMock.createStartContract(); overlays.banners.getComponent.mockReturnValue(
I'm a banner!
); - injectedMetadata = injectedMetadataServiceMock.createStartContract(); - targetDomElement = document.createElement('div'); rendering = new RenderingService(); @@ -56,20 +52,14 @@ describe('RenderingService#start', () => { return rendering.start({ application, chrome, - injectedMetadata, overlays, targetDomElement, }); }; - describe('standard mode', () => { - beforeEach(() => { - injectedMetadata.getLegacyMode.mockReturnValue(false); - }); - - it('renders application service into provided DOM element', () => { - startService(); - expect(targetDomElement.querySelector('div.application')).toMatchInlineSnapshot(` + it('renders application service into provided DOM element', () => { + startService(); + expect(targetDomElement.querySelector('div.application')).toMatchInlineSnapshot(`
@@ -78,50 +68,50 @@ describe('RenderingService#start', () => {
`); - }); + }); - it('adds the `chrome-hidden` class to the AppWrapper when chrome is hidden', () => { - const isVisible$ = new BehaviorSubject(true); - chrome.getIsVisible$.mockReturnValue(isVisible$); - startService(); + it('adds the `chrome-hidden` class to the AppWrapper when chrome is hidden', () => { + const isVisible$ = new BehaviorSubject(true); + chrome.getIsVisible$.mockReturnValue(isVisible$); + startService(); - const appWrapper = targetDomElement.querySelector('div.app-wrapper')!; - expect(appWrapper.className).toEqual('app-wrapper'); + const appWrapper = targetDomElement.querySelector('div.app-wrapper')!; + expect(appWrapper.className).toEqual('app-wrapper'); - act(() => isVisible$.next(false)); - expect(appWrapper.className).toEqual('app-wrapper hidden-chrome'); + act(() => isVisible$.next(false)); + expect(appWrapper.className).toEqual('app-wrapper hidden-chrome'); - act(() => isVisible$.next(true)); - expect(appWrapper.className).toEqual('app-wrapper'); - }); + act(() => isVisible$.next(true)); + expect(appWrapper.className).toEqual('app-wrapper'); + }); - it('adds the application classes to the AppContainer', () => { - const applicationClasses$ = new BehaviorSubject([]); - chrome.getApplicationClasses$.mockReturnValue(applicationClasses$); - startService(); + it('adds the application classes to the AppContainer', () => { + const applicationClasses$ = new BehaviorSubject([]); + chrome.getApplicationClasses$.mockReturnValue(applicationClasses$); + startService(); - const appContainer = targetDomElement.querySelector('div.application')!; - expect(appContainer.className).toEqual('application'); + const appContainer = targetDomElement.querySelector('div.application')!; + expect(appContainer.className).toEqual('application'); - act(() => applicationClasses$.next(['classA', 'classB'])); - expect(appContainer.className).toEqual('application classA classB'); + act(() => applicationClasses$.next(['classA', 'classB'])); + expect(appContainer.className).toEqual('application classA classB'); - act(() => applicationClasses$.next(['classC'])); - expect(appContainer.className).toEqual('application classC'); + act(() => applicationClasses$.next(['classC'])); + expect(appContainer.className).toEqual('application classC'); - act(() => applicationClasses$.next([])); - expect(appContainer.className).toEqual('application'); - }); + act(() => applicationClasses$.next([])); + expect(appContainer.className).toEqual('application'); + }); - it('contains wrapper divs', () => { - startService(); - expect(targetDomElement.querySelector('div.app-wrapper')).toBeDefined(); - expect(targetDomElement.querySelector('div.app-wrapper-pannel')).toBeDefined(); - }); + it('contains wrapper divs', () => { + startService(); + expect(targetDomElement.querySelector('div.app-wrapper')).toBeDefined(); + expect(targetDomElement.querySelector('div.app-wrapper-pannel')).toBeDefined(); + }); - it('renders the banner UI', () => { - startService(); - expect(targetDomElement.querySelector('#globalBannerList')).toMatchInlineSnapshot(` + it('renders the banner UI', () => { + startService(); + expect(targetDomElement.querySelector('#globalBannerList')).toMatchInlineSnapshot(`
@@ -130,36 +120,5 @@ describe('RenderingService#start', () => {
`); - }); - }); - - describe('legacy mode', () => { - beforeEach(() => { - injectedMetadata.getLegacyMode.mockReturnValue(true); - }); - - it('renders into provided DOM element', () => { - startService(); - - expect(targetDomElement).toMatchInlineSnapshot(` -
-
-
- Hello chrome! -
-
-
-
- `); - }); - - it('returns a div for the legacy service to render into', () => { - const { legacyTargetDomElement } = startService(); - - expect(targetDomElement.contains(legacyTargetDomElement!)).toBe(true); - }); }); }); diff --git a/src/core/public/rendering/rendering_service.tsx b/src/core/public/rendering/rendering_service.tsx index 58b8c1921e333..a20e14dbf61c5 100644 --- a/src/core/public/rendering/rendering_service.tsx +++ b/src/core/public/rendering/rendering_service.tsx @@ -23,14 +23,12 @@ import { I18nProvider } from '@kbn/i18n/react'; import { InternalChromeStart } from '../chrome'; import { InternalApplicationStart } from '../application'; -import { InjectedMetadataStart } from '../injected_metadata'; import { OverlayStart } from '../overlays'; import { AppWrapper, AppContainer } from './app_containers'; interface StartDeps { application: InternalApplicationStart; chrome: InternalChromeStart; - injectedMetadata: InjectedMetadataStart; overlays: OverlayStart; targetDomElement: HTMLDivElement; } @@ -41,53 +39,28 @@ interface StartDeps { * @internalRemarks Currently this only renders Chrome UI. Notifications and * Overlays UI should be moved here as well. * - * @returns a DOM element for the legacy platform to render into. - * * @internal */ export class RenderingService { - start({ - application, - chrome, - injectedMetadata, - overlays, - targetDomElement, - }: StartDeps): RenderingStart { + start({ application, chrome, overlays, targetDomElement }: StartDeps) { const chromeUi = chrome.getHeaderComponent(); const appUi = application.getComponent(); const bannerUi = overlays.banners.getComponent(); - const legacyMode = injectedMetadata.getLegacyMode(); - const legacyRef = legacyMode ? React.createRef() : null; - ReactDOM.render(
{chromeUi} - {!legacyMode && ( - -
-
{bannerUi}
- {appUi} -
-
- )} - - {legacyMode &&
} + +
+
{bannerUi}
+ {appUi} +
+
, targetDomElement ); - - return { - // When in legacy mode, return legacy div, otherwise undefined. - legacyTargetDomElement: legacyRef ? legacyRef.current! : undefined, - }; } } - -/** @internal */ -export interface RenderingStart { - legacyTargetDomElement?: HTMLDivElement; -} diff --git a/src/core/public/ui_settings/ui_settings_api.test.ts b/src/core/public/ui_settings/ui_settings_api.test.ts index 14791407d2550..b15754e5f1383 100644 --- a/src/core/public/ui_settings/ui_settings_api.test.ts +++ b/src/core/public/ui_settings/ui_settings_api.test.ts @@ -22,7 +22,7 @@ import fetchMock from 'fetch-mock/es5/client'; import * as Rx from 'rxjs'; import { takeUntil, toArray } from 'rxjs/operators'; -import { setup as httpSetup } from '../../../test_utils/public/http_test_setup'; +import { setup as httpSetup } from '../../test_helpers/http_test_setup'; import { UiSettingsApi } from './ui_settings_api'; function setup() { diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts index 65f5bbdac5248..3ebf6d507a2fd 100644 --- a/src/core/server/config/integration_tests/config_deprecation.test.ts +++ b/src/core/server/config/integration_tests/config_deprecation.test.ts @@ -19,7 +19,7 @@ import { mockLoggingSystem } from './config_deprecation.test.mocks'; import { loggingSystemMock } from '../../logging/logging_system.mock'; -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; describe('configuration deprecations', () => { let root: ReturnType; diff --git a/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts index 3284be5ba4750..340f45a0a2c18 100644 --- a/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts +++ b/src/core/server/core_app/integration_tests/default_route_provider_config.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { Root } from '../../root'; const { startES } = kbnTestServer.createTestServers({ diff --git a/src/core/server/core_app/integration_tests/static_assets.test.ts b/src/core/server/core_app/integration_tests/static_assets.test.ts index 160ef064a14d9..ca03c4228221f 100644 --- a/src/core/server/core_app/integration_tests/static_assets.test.ts +++ b/src/core/server/core_app/integration_tests/static_assets.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { Root } from '../../root'; describe('Platform assets', function () { diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 2b9193a280aec..f30ff66ed803a 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -30,7 +30,7 @@ import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy'; import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { InternalElasticsearchServiceStart } from '../../elasticsearch'; interface User { diff --git a/src/core/server/http_resources/integration_tests/http_resources_service.test.ts b/src/core/server/http_resources/integration_tests/http_resources_service.test.ts index eee7dc2786076..624cdbb7f9655 100644 --- a/src/core/server/http_resources/integration_tests/http_resources_service.test.ts +++ b/src/core/server/http_resources/integration_tests/http_resources_service.test.ts @@ -17,7 +17,7 @@ * under the License. */ import { schema } from '@kbn/config-schema'; -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; describe('http resources service', () => { describe('register', () => { diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts index 2106a0748d814..f8506b5744030 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.test.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.test.ts @@ -217,34 +217,4 @@ describe('getUnusedConfigKeys', () => { }) ).toEqual([]); }); - - describe('using deprecation', () => { - it('should use the plugin deprecations provider', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - pluginSpecs: [ - ({ - getDeprecationsProvider() { - return async ({ rename }: any) => [rename('foo1', 'foo2')]; - }, - getConfigPrefix: () => 'foo', - } as unknown) as LegacyPluginSpec, - ], - disabledPluginSpecs: [], - settings: { - foo: { - foo: 'dolly', - foo1: 'bar', - }, - }, - legacyConfig: getConfig({ - foo: { - foo2: 'bar', - }, - }), - }) - ).toEqual(['foo.foo']); - }); - }); }); diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts index 354bf9af042cf..d10c574f04974 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.ts @@ -17,10 +17,7 @@ * under the License. */ -import { set } from '@elastic/safer-lodash-set'; -import { difference, get } from 'lodash'; -// @ts-expect-error -import { getTransform } from '../../../../legacy/deprecation/index'; +import { difference } from 'lodash'; import { unset } from '../../../../legacy/utils'; import { getFlattenedObject } from '../../../utils'; import { hasConfigPathIntersection } from '../../config'; @@ -41,21 +38,6 @@ export async function getUnusedConfigKeys({ settings: LegacyVars; legacyConfig: LegacyConfig; }) { - // transform deprecated plugin settings - for (let i = 0; i < pluginSpecs.length; i++) { - const spec = pluginSpecs[i]; - const transform = await getTransform(spec); - const prefix = spec.getConfigPrefix(); - - // nested plugin prefixes (a.b) translate to nested objects - const pluginSettings = get(settings, prefix); - if (pluginSettings) { - // flattened settings are expected to be converted to nested objects - // a.b = true => { a: { b: true }} - set(settings, prefix, transform(pluginSettings)); - } - } - // remove config values from disabled plugins for (const spec of disabledPluginSpecs) { unset(settings, spec.getConfigPrefix()); diff --git a/src/core/server/legacy/integration_tests/legacy_service.test.ts b/src/core/server/legacy/integration_tests/legacy_service.test.ts index 1dc8d53e7c3d6..ca3573e730d3f 100644 --- a/src/core/server/legacy/integration_tests/legacy_service.test.ts +++ b/src/core/server/legacy/integration_tests/legacy_service.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; describe('legacy service', () => { describe('http server', () => { diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts index 2581c85debf26..2ebe17ea92978 100644 --- a/src/core/server/legacy/integration_tests/logging.test.ts +++ b/src/core/server/legacy/integration_tests/logging.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { getPlatformLogsFromMock, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 7d5557be92b30..adfdecdd7c976 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -323,9 +323,6 @@ export class LegacyService implements CoreService { status: { core$: setupDeps.core.status.core$, overall$: setupDeps.core.status.overall$, - set: setupDeps.core.status.plugins.set.bind(null, 'legacy'), - dependencies$: setupDeps.core.status.plugins.getDependenciesStatus$('legacy'), - derivedStatus$: setupDeps.core.status.plugins.getDerivedStatus$('legacy'), }, uiSettings: { register: setupDeps.core.uiSettings.register, diff --git a/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap b/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap deleted file mode 100644 index c1b7164908ed6..0000000000000 --- a/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap +++ /dev/null @@ -1,56 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` 1`] = ` -Array [ - Object { - "category": undefined, - "disableSubUrlTracking": undefined, - "disabled": false, - "euiIconType": undefined, - "hidden": false, - "icon": undefined, - "id": "link-a", - "linkToLastSubUrl": true, - "order": 0, - "subUrlBase": "/some-custom-url", - "title": "AppA", - "tooltip": "", - "url": "/some-custom-url", - }, - Object { - "category": undefined, - "disableSubUrlTracking": true, - "disabled": false, - "euiIconType": undefined, - "hidden": false, - "icon": undefined, - "id": "link-b", - "linkToLastSubUrl": true, - "order": 0, - "subUrlBase": "/url-b", - "title": "AppB", - "tooltip": "", - "url": "/url-b", - }, - Object { - "category": undefined, - "euiIconType": undefined, - "icon": undefined, - "id": "app-a", - "linkToLastSubUrl": true, - "order": 0, - "title": "AppA", - "url": "/app/app-a", - }, - Object { - "category": undefined, - "euiIconType": undefined, - "icon": undefined, - "id": "app-b", - "linkToLastSubUrl": true, - "order": 0, - "title": "AppB", - "url": "/app/app-b", - }, -] -`; diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index f3ec2ed8335c5..82e04496ffc3e 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -31,7 +31,6 @@ import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/u import { LoggerFactory } from '../../logging'; import { PackageInfo } from '../../config'; import { LegacyPluginSpec, LegacyPluginPack, LegacyConfig } from '../types'; -import { getNavLinks } from './get_nav_links'; export async function findLegacyPluginSpecs( settings: unknown, @@ -125,13 +124,12 @@ export async function findLegacyPluginSpecs( log$.pipe(toArray()) ).toPromise(); const uiExports = collectLegacyUiExports(pluginSpecs); - const navLinks = getNavLinks(uiExports, pluginSpecs); return { disabledPluginSpecs, pluginSpecs, pluginExtendedConfig: configToMutate, uiExports, - navLinks, + navLinks: [], }; } diff --git a/src/core/server/legacy/plugins/get_nav_links.test.ts b/src/core/server/legacy/plugins/get_nav_links.test.ts deleted file mode 100644 index af10706d0ea08..0000000000000 --- a/src/core/server/legacy/plugins/get_nav_links.test.ts +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { LegacyUiExports, LegacyPluginSpec, LegacyAppSpec, LegacyNavLinkSpec } from '../types'; -import { getNavLinks } from './get_nav_links'; - -const createLegacyExports = ({ - uiAppSpecs = [], - navLinkSpecs = [], -}: { - uiAppSpecs?: LegacyAppSpec[]; - navLinkSpecs?: LegacyNavLinkSpec[]; -}): LegacyUiExports => ({ - uiAppSpecs, - navLinkSpecs, - injectedVarsReplacers: [], - defaultInjectedVarProviders: [], - savedObjectMappings: [], - savedObjectSchemas: {}, - savedObjectMigrations: {}, - savedObjectValidations: {}, - savedObjectsManagement: {}, -}); - -const createPluginSpecs = (...ids: string[]): LegacyPluginSpec[] => - ids.map( - (id) => - ({ - getId: () => id, - } as LegacyPluginSpec) - ); - -describe('getNavLinks', () => { - describe('generating from uiAppSpecs', () => { - it('generates navlinks from legacy app specs', () => { - const navlinks = getNavLinks( - createLegacyExports({ - uiAppSpecs: [ - { - id: 'app-a', - title: 'AppA', - pluginId: 'pluginA', - }, - { - id: 'app-b', - title: 'AppB', - pluginId: 'pluginA', - }, - ], - }), - createPluginSpecs('pluginA') - ); - - expect(navlinks.length).toEqual(2); - expect(navlinks[0]).toEqual( - expect.objectContaining({ - id: 'app-a', - title: 'AppA', - url: '/app/app-a', - }) - ); - expect(navlinks[1]).toEqual( - expect.objectContaining({ - id: 'app-b', - title: 'AppB', - url: '/app/app-b', - }) - ); - }); - - it('uses the app id to generates the navlink id even if pluginId is specified', () => { - const navlinks = getNavLinks( - createLegacyExports({ - uiAppSpecs: [ - { - id: 'app-a', - title: 'AppA', - pluginId: 'pluginA', - }, - { - id: 'app-b', - title: 'AppB', - pluginId: 'pluginA', - }, - ], - }), - createPluginSpecs('pluginA') - ); - - expect(navlinks.length).toEqual(2); - expect(navlinks[0].id).toEqual('app-a'); - expect(navlinks[1].id).toEqual('app-b'); - }); - - it('throws if an app reference a missing plugin', () => { - expect(() => { - getNavLinks( - createLegacyExports({ - uiAppSpecs: [ - { - id: 'app-a', - title: 'AppA', - pluginId: 'notExistingPlugin', - }, - ], - }), - createPluginSpecs('pluginA') - ); - }).toThrowErrorMatchingInlineSnapshot(`"Unknown plugin id \\"notExistingPlugin\\""`); - }); - - it('uses all known properties of the navlink', () => { - const navlinks = getNavLinks( - createLegacyExports({ - uiAppSpecs: [ - { - id: 'app-a', - title: 'AppA', - category: { - id: 'foo', - label: 'My Category', - }, - order: 42, - url: '/some-custom-url', - icon: 'fa-snowflake', - euiIconType: 'euiIcon', - linkToLastSubUrl: true, - hidden: false, - }, - ], - }), - [] - ); - expect(navlinks.length).toBe(1); - expect(navlinks[0]).toEqual({ - id: 'app-a', - title: 'AppA', - category: { - id: 'foo', - label: 'My Category', - }, - order: 42, - url: '/some-custom-url', - icon: 'fa-snowflake', - euiIconType: 'euiIcon', - linkToLastSubUrl: true, - }); - }); - }); - - describe('generating from navLinkSpecs', () => { - it('generates navlinks from legacy navLink specs', () => { - const navlinks = getNavLinks( - createLegacyExports({ - navLinkSpecs: [ - { - id: 'link-a', - title: 'AppA', - url: '/some-custom-url', - }, - { - id: 'link-b', - title: 'AppB', - url: '/some-other-url', - disableSubUrlTracking: true, - }, - ], - }), - createPluginSpecs('pluginA') - ); - - expect(navlinks.length).toEqual(2); - expect(navlinks[0]).toEqual( - expect.objectContaining({ - id: 'link-a', - title: 'AppA', - url: '/some-custom-url', - hidden: false, - disabled: false, - }) - ); - expect(navlinks[1]).toEqual( - expect.objectContaining({ - id: 'link-b', - title: 'AppB', - url: '/some-other-url', - disableSubUrlTracking: true, - }) - ); - }); - - it('only uses known properties to create the navlink', () => { - const navlinks = getNavLinks( - createLegacyExports({ - navLinkSpecs: [ - { - id: 'link-a', - title: 'AppA', - category: { - id: 'foo', - label: 'My Second Cat', - }, - order: 72, - url: '/some-other-custom', - subUrlBase: '/some-other-custom/sub', - disableSubUrlTracking: true, - icon: 'fa-corn', - euiIconType: 'euiIconBis', - linkToLastSubUrl: false, - hidden: false, - tooltip: 'My other tooltip', - }, - ], - }), - [] - ); - expect(navlinks.length).toBe(1); - expect(navlinks[0]).toEqual({ - id: 'link-a', - title: 'AppA', - category: { - id: 'foo', - label: 'My Second Cat', - }, - order: 72, - url: '/some-other-custom', - subUrlBase: '/some-other-custom/sub', - disableSubUrlTracking: true, - icon: 'fa-corn', - euiIconType: 'euiIconBis', - linkToLastSubUrl: false, - hidden: false, - disabled: false, - tooltip: 'My other tooltip', - }); - }); - }); - - describe('generating from both apps and navlinks', () => { - const navlinks = getNavLinks( - createLegacyExports({ - uiAppSpecs: [ - { - id: 'app-a', - title: 'AppA', - }, - { - id: 'app-b', - title: 'AppB', - }, - ], - navLinkSpecs: [ - { - id: 'link-a', - title: 'AppA', - url: '/some-custom-url', - }, - { - id: 'link-b', - title: 'AppB', - url: '/url-b', - disableSubUrlTracking: true, - }, - ], - }), - [] - ); - - expect(navlinks.length).toBe(4); - expect(navlinks).toMatchSnapshot(); - }); -}); diff --git a/src/core/server/legacy/plugins/get_nav_links.ts b/src/core/server/legacy/plugins/get_nav_links.ts deleted file mode 100644 index b1d22df41e345..0000000000000 --- a/src/core/server/legacy/plugins/get_nav_links.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - LegacyUiExports, - LegacyNavLink, - LegacyPluginSpec, - LegacyNavLinkSpec, - LegacyAppSpec, -} from '../types'; - -function legacyAppToNavLink(spec: LegacyAppSpec): LegacyNavLink { - if (!spec.id) { - throw new Error('Every app must specify an id'); - } - return { - id: spec.id, - category: spec.category, - title: spec.title ?? spec.id, - order: typeof spec.order === 'number' ? spec.order : 0, - icon: spec.icon, - euiIconType: spec.euiIconType, - url: spec.url || `/app/${spec.id}`, - linkToLastSubUrl: spec.linkToLastSubUrl ?? true, - }; -} - -function legacyLinkToNavLink(spec: LegacyNavLinkSpec): LegacyNavLink { - return { - id: spec.id, - category: spec.category, - title: spec.title, - order: typeof spec.order === 'number' ? spec.order : 0, - url: spec.url, - subUrlBase: spec.subUrlBase || spec.url, - disableSubUrlTracking: spec.disableSubUrlTracking, - icon: spec.icon, - euiIconType: spec.euiIconType, - linkToLastSubUrl: spec.linkToLastSubUrl ?? true, - hidden: spec.hidden ?? false, - disabled: spec.disabled ?? false, - tooltip: spec.tooltip ?? '', - }; -} - -function isHidden(app: LegacyAppSpec) { - return app.listed === false || app.hidden === true; -} - -export function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) { - const navLinkSpecs = uiExports.navLinkSpecs || []; - const appSpecs = (uiExports.uiAppSpecs || []).filter( - (app) => app !== undefined && !isHidden(app) - ) as LegacyAppSpec[]; - - const pluginIds = (pluginSpecs || []).map((spec) => spec.getId()); - appSpecs.forEach((spec) => { - if (spec.pluginId && !pluginIds.includes(spec.pluginId)) { - throw new Error(`Unknown plugin id "${spec.pluginId}"`); - } - }); - - return [...navLinkSpecs.map(legacyLinkToNavLink), ...appSpecs.map(legacyAppToNavLink)].sort( - (a, b) => a.order - b.order - ); -} diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index 841c1ce15af47..7f6059567c46e 100644 --- a/src/core/server/logging/integration_tests/logging.test.ts +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { InternalCoreSetup } from '../../internal_types'; import { LoggerContextConfigInput } from '../logging_config'; import { Subject } from 'rxjs'; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index eb31b2380d177..fa2659ca130a0 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -185,9 +185,6 @@ export function createPluginSetupContext( status: { core$: deps.status.core$, overall$: deps.status.overall$, - set: deps.status.plugins.set.bind(null, plugin.name), - dependencies$: deps.status.plugins.getDependenciesStatus$(plugin.name), - derivedStatus$: deps.status.plugins.getDerivedStatus$(plugin.name), }, uiSettings: { register: deps.uiSettings.register, diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 71ac31db13f92..7af77491df1ab 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -100,27 +100,15 @@ test('getPluginDependencies returns dependency tree of symbols', () => { pluginsSystem.addPlugin(createPlugin('no-dep')); expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(` - Object { - "asNames": Map { - "plugin-a" => Array [ - "no-dep", - ], - "plugin-b" => Array [ - "plugin-a", - "no-dep", - ], - "no-dep" => Array [], - }, - "asOpaqueIds": Map { - Symbol(plugin-a) => Array [ - Symbol(no-dep), - ], - Symbol(plugin-b) => Array [ - Symbol(plugin-a), - Symbol(no-dep), - ], - Symbol(no-dep) => Array [], - }, + Map { + Symbol(plugin-a) => Array [ + Symbol(no-dep), + ], + Symbol(plugin-b) => Array [ + Symbol(plugin-a), + Symbol(no-dep), + ], + Symbol(no-dep) => Array [], } `); }); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index b2acd9a6fd04b..f5c1b35d678a3 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -20,11 +20,10 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, PluginName } from './types'; +import { DiscoveredPlugin, PluginName, PluginOpaqueId } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; import { withTimeout } from '../../utils'; -import { PluginDependencies } from '.'; const Sec = 1000; /** @internal */ @@ -46,19 +45,9 @@ export class PluginsSystem { * @returns a ReadonlyMap of each plugin and an Array of its available dependencies * @internal */ - public getPluginDependencies(): PluginDependencies { - const asNames = new Map( - [...this.plugins].map(([name, plugin]) => [ - plugin.name, - [ - ...new Set([ - ...plugin.requiredPlugins, - ...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)), - ]), - ].map((depId) => this.plugins.get(depId)!.name), - ]) - ); - const asOpaqueIds = new Map( + public getPluginDependencies(): ReadonlyMap { + // Return dependency map of opaque ids + return new Map( [...this.plugins].map(([name, plugin]) => [ plugin.opaqueId, [ @@ -69,8 +58,6 @@ export class PluginsSystem { ].map((depId) => this.plugins.get(depId)!.opaqueId), ]) ); - - return { asNames, asOpaqueIds }; } public async setupPlugins(deps: PluginsServiceSetupDeps) { diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 517261b5bc9bb..eb2a9ca3daf5f 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -93,12 +93,6 @@ export type PluginName = string; /** @public */ export type PluginOpaqueId = symbol; -/** @internal */ -export interface PluginDependencies { - asNames: ReadonlyMap; - asOpaqueIds: ReadonlyMap; -} - /** * Describes the set of required and optional properties plugin can define in its * mandatory JSON manifest file. diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 5ff5d69f96f70..ab828a1780425 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -46,7 +46,6 @@ Object { }, "version": Any, }, - "legacyMode": false, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -100,7 +99,6 @@ Object { }, "version": Any, }, - "legacyMode": false, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -158,7 +156,6 @@ Object { }, "version": Any, }, - "legacyMode": false, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -212,7 +209,6 @@ Object { }, "version": Any, }, - "legacyMode": false, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, @@ -266,7 +262,6 @@ Object { }, "version": Any, }, - "legacyMode": false, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], "vars": Object {}, diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index e7ee0b16fce08..7761c89044f6f 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -82,7 +82,6 @@ export class RenderingService implements CoreService { diff --git a/src/core/server/saved_objects/routes/export.ts b/src/core/server/saved_objects/routes/export.ts index 9445c144ecda4..35a65d8d9651f 100644 --- a/src/core/server/saved_objects/routes/export.ts +++ b/src/core/server/saved_objects/routes/export.ts @@ -19,11 +19,7 @@ import { schema } from '@kbn/config-schema'; import stringify from 'json-stable-stringify'; -import { - createPromiseFromStreams, - createMapStream, - createConcatStream, -} from '../../../../legacy/utils/streams'; +import { createPromiseFromStreams, createMapStream, createConcatStream } from '../../utils/streams'; import { IRouter } from '../../http'; import { SavedObjectConfig } from '../saved_objects_config'; import { exportSavedObjectsToStream } from '../export'; diff --git a/src/core/server/saved_objects/routes/integration_tests/export.test.ts b/src/core/server/saved_objects/routes/integration_tests/export.test.ts index d47f7c6050d8f..a3891712fd22b 100644 --- a/src/core/server/saved_objects/routes/integration_tests/export.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/export.test.ts @@ -22,7 +22,7 @@ jest.mock('../../export', () => ({ })); import * as exportMock from '../../export'; -import { createListStream } from '../../../../../legacy/utils/streams'; +import { createListStream } from '../../../utils/streams'; import supertest from 'supertest'; import { UnwrapPromise } from '@kbn/utility-types'; import { SavedObjectConfig } from '../../saved_objects_config'; diff --git a/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts b/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts index 7a0e39b71afb8..e003d564c1ea2 100644 --- a/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/migrate.test.ts @@ -18,7 +18,7 @@ */ import { migratorInstanceMock } from './migrate.test.mocks'; -import * as kbnTestServer from '../../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../../test_helpers/kbn_server'; describe('SavedObjects /_migrate endpoint', () => { let root: ReturnType; diff --git a/src/core/server/saved_objects/routes/utils.test.ts b/src/core/server/saved_objects/routes/utils.test.ts index 24719724785af..fd3bdad8606ed 100644 --- a/src/core/server/saved_objects/routes/utils.test.ts +++ b/src/core/server/saved_objects/routes/utils.test.ts @@ -19,7 +19,7 @@ import { createSavedObjectsStreamFromNdJson, validateTypes, validateObjects } from './utils'; import { Readable } from 'stream'; -import { createPromiseFromStreams, createConcatStream } from '../../../../legacy/utils/streams'; +import { createPromiseFromStreams, createConcatStream } from '../../utils/streams'; async function readStreamToCompletion(stream: Readable) { return createPromiseFromStreams([stream, createConcatStream([])]); diff --git a/src/core/server/saved_objects/routes/utils.ts b/src/core/server/saved_objects/routes/utils.ts index 3963833a9c718..f16a6e471257d 100644 --- a/src/core/server/saved_objects/routes/utils.ts +++ b/src/core/server/saved_objects/routes/utils.ts @@ -19,11 +19,7 @@ import { Readable } from 'stream'; import { SavedObject, SavedObjectsExportResultDetails } from 'src/core/server'; -import { - createSplitStream, - createMapStream, - createFilterStream, -} from '../../../../legacy/utils/streams'; +import { createSplitStream, createMapStream, createFilterStream } from '../../utils/streams'; export function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) { return ndJsonStream diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 05afad5a4f7a4..1123433e30ac5 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -40,6 +40,7 @@ import { DeleteScriptParams } from 'elasticsearch'; import { DeleteTemplateParams } from 'elasticsearch'; import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; +import { ErrorToastOptions } from 'src/core/public/notifications'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; import { FieldStatsParams } from 'elasticsearch'; @@ -118,6 +119,7 @@ import { RenderSearchTemplateParams } from 'elasticsearch'; import { Request } from 'hapi'; import { ResponseObject } from 'hapi'; import { ResponseToolkit } from 'hapi'; +import { SavedObject as SavedObject_2 } from 'src/core/server'; import { SchemaTypeError } from '@kbn/config-schema'; import { ScrollParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch'; @@ -141,6 +143,7 @@ import { TasksCancelParams } from 'elasticsearch'; import { TasksGetParams } from 'elasticsearch'; import { TasksListParams } from 'elasticsearch'; import { TermvectorsParams } from 'elasticsearch'; +import { ToastInputFields } from 'src/core/public/notifications'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; @@ -2855,17 +2858,10 @@ export type SharedGlobalConfig = RecursiveReadonly<{ // @public export type StartServicesAccessor = () => Promise<[CoreStart, TPluginsStart, TStart]>; -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ServiceStatusSetup" -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ServiceStatusSetup" -// // @public export interface StatusServiceSetup { core$: Observable; - dependencies$: Observable>; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "StatusSetup" - derivedStatus$: Observable; overall$: Observable; - set(status$: Observable): void; } // @public @@ -2958,8 +2954,8 @@ export const validBodyOutput: readonly ["data", "stream"]; // src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:166:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:167:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:274:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:268:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 1bd364c2f87b7..417f66a2988c2 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -41,7 +41,6 @@ import { Server } from './server'; import { getEnvOptions } from './config/__mocks__/env'; import { loggingSystemMock } from './logging/logging_system.mock'; import { rawConfigServiceMock } from './config/raw_config_service.mock'; -import { PluginName } from './plugins'; const env = new Env('.', getEnvOptions()); const logger = loggingSystemMock.create(); @@ -50,7 +49,7 @@ const rawConfigService = rawConfigServiceMock.create({}); beforeEach(() => { mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); mockPluginsService.discover.mockResolvedValue({ - pluginTree: { asOpaqueIds: new Map(), asNames: new Map() }, + pluginTree: new Map(), uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); }); @@ -99,7 +98,7 @@ test('injects legacy dependency to context#setup()', async () => { [pluginB, [pluginA]], ]); mockPluginsService.discover.mockResolvedValue({ - pluginTree: { asOpaqueIds: pluginDependencies, asNames: new Map() }, + pluginTree: pluginDependencies, uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); @@ -114,31 +113,6 @@ test('injects legacy dependency to context#setup()', async () => { }); }); -test('injects legacy dependency to status#setup()', async () => { - const server = new Server(rawConfigService, env, logger); - - const pluginDependencies = new Map([ - ['a', []], - ['b', ['a']], - ]); - mockPluginsService.discover.mockResolvedValue({ - pluginTree: { asOpaqueIds: new Map(), asNames: pluginDependencies }, - uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, - }); - - await server.setup(); - - expect(mockStatusService.setup).toHaveBeenCalledWith({ - elasticsearch: expect.any(Object), - savedObjects: expect.any(Object), - pluginDependencies: new Map([ - ['a', []], - ['b', ['a']], - ['legacy', ['a', 'b']], - ]), - }); -}); - test('runs services on "start"', async () => { const server = new Server(rawConfigService, env, logger); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index e2f77f0551f34..cc6d8171e7a03 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -121,13 +121,10 @@ export class Server { const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: - // 1) Can access context from any KP plugin + // 1) Can access context from any NP plugin // 2) Can register context providers that will only be available to other legacy plugins and will not leak into // New Platform plugins. - pluginDependencies: new Map([ - ...pluginTree.asOpaqueIds, - [this.legacy.legacyId, [...pluginTree.asOpaqueIds.keys()]], - ]), + pluginDependencies: new Map([...pluginTree, [this.legacy.legacyId, [...pluginTree.keys()]]]), }); const auditTrailSetup = this.auditTrail.setup(); @@ -157,12 +154,6 @@ export class Server { const statusSetup = await this.status.setup({ elasticsearch: elasticsearchServiceSetup, - // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy can access plugin status from - // any KP plugin - pluginDependencies: new Map([ - ...pluginTree.asNames, - ['legacy', [...pluginTree.asNames.keys()]], - ]), savedObjects: savedObjectsSetup, }); diff --git a/src/core/server/status/get_summary_status.test.ts b/src/core/server/status/get_summary_status.test.ts index d97083162b502..7516e82ee784d 100644 --- a/src/core/server/status/get_summary_status.test.ts +++ b/src/core/server/status/get_summary_status.test.ts @@ -94,38 +94,6 @@ describe('getSummaryStatus', () => { describe('summary', () => { describe('when a single service is at highest level', () => { it('returns all information about that single service', () => { - expect( - getSummaryStatus( - Object.entries({ - s1: degraded, - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - meta: { - custom: { data: 'here' }, - }, - }, - }) - ) - ).toEqual({ - level: ServiceStatusLevels.unavailable, - summary: '[s2]: Lorem ipsum', - detail: 'See the status page for more information', - meta: { - affectedServices: { - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - meta: { - custom: { data: 'here' }, - }, - }, - }, - }, - }); - }); - - it('allows the single service to override the detail and documentationUrl fields', () => { expect( getSummaryStatus( Object.entries({ @@ -147,17 +115,7 @@ describe('getSummaryStatus', () => { detail: 'Vivamus pulvinar sem ac luctus ultrices.', documentationUrl: 'http://helpmenow.com/problem1', meta: { - affectedServices: { - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - detail: 'Vivamus pulvinar sem ac luctus ultrices.', - documentationUrl: 'http://helpmenow.com/problem1', - meta: { - custom: { data: 'here' }, - }, - }, - }, + custom: { data: 'here' }, }, }); }); diff --git a/src/core/server/status/get_summary_status.ts b/src/core/server/status/get_summary_status.ts index 1dc92839e8261..748a54f0bf8bb 100644 --- a/src/core/server/status/get_summary_status.ts +++ b/src/core/server/status/get_summary_status.ts @@ -23,10 +23,7 @@ import { ServiceStatus, ServiceStatusLevels, ServiceStatusLevel } from './types' * Returns a single {@link ServiceStatus} that summarizes the most severe status level from a group of statuses. * @param statuses */ -export const getSummaryStatus = ( - statuses: Array<[string, ServiceStatus]>, - { allAvailableSummary = `All services are available` }: { allAvailableSummary?: string } = {} -): ServiceStatus => { +export const getSummaryStatus = (statuses: Array<[string, ServiceStatus]>): ServiceStatus => { const grouped = groupByLevel(statuses); const highestSeverityLevel = getHighestSeverityLevel(grouped.keys()); const highestSeverityGroup = grouped.get(highestSeverityLevel)!; @@ -34,18 +31,13 @@ export const getSummaryStatus = ( if (highestSeverityLevel === ServiceStatusLevels.available) { return { level: ServiceStatusLevels.available, - summary: allAvailableSummary, + summary: `All services are available`, }; } else if (highestSeverityGroup.size === 1) { const [serviceName, status] = [...highestSeverityGroup.entries()][0]; return { ...status, summary: `[${serviceName}]: ${status.summary!}`, - // TODO: include URL to status page - detail: status.detail ?? `See the status page for more information`, - meta: { - affectedServices: { [serviceName]: status }, - }, }; } else { return { diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts deleted file mode 100644 index b2d2ac8a5ef90..0000000000000 --- a/src/core/server/status/plugins_status.test.ts +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginName } from '../plugins'; -import { PluginsStatusService } from './plugins_status'; -import { of, Observable, BehaviorSubject } from 'rxjs'; -import { ServiceStatusLevels, CoreStatus, ServiceStatus } from './types'; -import { first } from 'rxjs/operators'; -import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; - -expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); - -describe('PluginStatusService', () => { - const coreAllAvailable$: Observable = of({ - elasticsearch: { level: ServiceStatusLevels.available, summary: 'elasticsearch avail' }, - savedObjects: { level: ServiceStatusLevels.available, summary: 'savedObjects avail' }, - }); - const coreOneDegraded$: Observable = of({ - elasticsearch: { level: ServiceStatusLevels.available, summary: 'elasticsearch avail' }, - savedObjects: { level: ServiceStatusLevels.degraded, summary: 'savedObjects degraded' }, - }); - const coreOneCriticalOneDegraded$: Observable = of({ - elasticsearch: { level: ServiceStatusLevels.critical, summary: 'elasticsearch critical' }, - savedObjects: { level: ServiceStatusLevels.degraded, summary: 'savedObjects degraded' }, - }); - const pluginDependencies: Map = new Map([ - ['a', []], - ['b', ['a']], - ['c', ['a', 'b']], - ]); - - describe('getDerivedStatus$', () => { - it(`defaults to core's most severe status`, async () => { - const serviceAvailable = new PluginsStatusService({ - core$: coreAllAvailable$, - pluginDependencies, - }); - expect(await serviceAvailable.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.available, - summary: 'All dependencies are available', - }); - - const serviceDegraded = new PluginsStatusService({ - core$: coreOneDegraded$, - pluginDependencies, - }); - expect(await serviceDegraded.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }); - - const serviceCritical = new PluginsStatusService({ - core$: coreOneCriticalOneDegraded$, - pluginDependencies, - }); - expect(await serviceCritical.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.critical, - summary: '[elasticsearch]: elasticsearch critical', - detail: 'See the status page for more information', - meta: expect.any(Object), - }); - }); - - it(`provides a summary status when core and dependencies are at same severity level`, async () => { - const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); - service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' })); - expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }); - }); - - it(`allows dependencies status to take precedence over lower severity core statuses`, async () => { - const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); - service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); - expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.unavailable, - summary: '[a]: a is not working', - detail: 'See the status page for more information', - meta: expect.any(Object), - }); - }); - - it(`allows core status to take precedence over lower severity dependencies statuses`, async () => { - const service = new PluginsStatusService({ - core$: coreOneCriticalOneDegraded$, - pluginDependencies, - }); - service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); - expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.critical, - summary: '[elasticsearch]: elasticsearch critical', - detail: 'See the status page for more information', - meta: expect.any(Object), - }); - }); - - it(`allows a severe dependency status to take precedence over a less severe dependency status`, async () => { - const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); - service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' })); - service.set('b', of({ level: ServiceStatusLevels.unavailable, summary: 'b is not working' })); - expect(await service.getDerivedStatus$('c').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.unavailable, - summary: '[b]: b is not working', - detail: 'See the status page for more information', - meta: expect.any(Object), - }); - }); - }); - - describe('getAll$', () => { - it('defaults to empty record if no plugins', async () => { - const service = new PluginsStatusService({ - core$: coreAllAvailable$, - pluginDependencies: new Map(), - }); - expect(await service.getAll$().pipe(first()).toPromise()).toEqual({}); - }); - - it('defaults to core status when no plugin statuses are set', async () => { - const serviceAvailable = new PluginsStatusService({ - core$: coreAllAvailable$, - pluginDependencies, - }); - expect(await serviceAvailable.getAll$().pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - c: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - }); - - const serviceDegraded = new PluginsStatusService({ - core$: coreOneDegraded$, - pluginDependencies, - }); - expect(await serviceDegraded.getAll$().pipe(first()).toPromise()).toEqual({ - a: { - level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - b: { - level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - c: { - level: ServiceStatusLevels.degraded, - summary: '[3] services are degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - }); - - const serviceCritical = new PluginsStatusService({ - core$: coreOneCriticalOneDegraded$, - pluginDependencies, - }); - expect(await serviceCritical.getAll$().pipe(first()).toPromise()).toEqual({ - a: { - level: ServiceStatusLevels.critical, - summary: '[elasticsearch]: elasticsearch critical', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - b: { - level: ServiceStatusLevels.critical, - summary: '[2] services are critical', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - c: { - level: ServiceStatusLevels.critical, - summary: '[3] services are critical', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - }); - }); - - it('uses the manually set status level if plugin specifies one', async () => { - const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); - service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' })); - - expect(await service.getAll$().pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded - b: { - level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - c: { - level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - }); - }); - - it('updates when a new plugin status observable is set', async () => { - const service = new PluginsStatusService({ - core$: coreAllAvailable$, - pluginDependencies: new Map([['a', []]]), - }); - const statusUpdates: Array> = []; - const subscription = service - .getAll$() - .subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses)); - - service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a degraded' })); - service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a unavailable' })); - service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a available' })); - subscription.unsubscribe(); - - expect(statusUpdates).toEqual([ - { a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } }, - { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } }, - { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } }, - { a: { level: ServiceStatusLevels.available, summary: 'a available' } }, - ]); - }); - }); - - describe('getDependenciesStatus$', () => { - it('only includes dependencies of specified plugin', async () => { - const service = new PluginsStatusService({ - core$: coreAllAvailable$, - pluginDependencies, - }); - expect(await service.getDependenciesStatus$('a').pipe(first()).toPromise()).toEqual({}); - expect(await service.getDependenciesStatus$('b').pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - }); - expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - }); - }); - - it('uses the manually set status level if plugin specifies one', async () => { - const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); - service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' })); - - expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded - b: { - level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - }); - }); - - it('throws error if unknown plugin passed', () => { - const service = new PluginsStatusService({ core$: coreAllAvailable$, pluginDependencies }); - expect(() => { - service.getDependenciesStatus$('dont-exist'); - }).toThrowError(); - }); - - it('debounces events in quick succession', async () => { - const service = new PluginsStatusService({ - core$: coreAllAvailable$, - pluginDependencies, - }); - const available: ServiceStatus = { - level: ServiceStatusLevels.available, - summary: 'a available', - }; - const degraded: ServiceStatus = { - level: ServiceStatusLevels.degraded, - summary: 'a degraded', - }; - const pluginA$ = new BehaviorSubject(available); - service.set('a', pluginA$); - - const statusUpdates: Array> = []; - const subscription = service - .getDependenciesStatus$('b') - .subscribe((status) => statusUpdates.push(status)); - const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - - pluginA$.next(degraded); - pluginA$.next(available); - pluginA$.next(degraded); - pluginA$.next(available); - pluginA$.next(degraded); - pluginA$.next(available); - pluginA$.next(degraded); - // Waiting for the debounce timeout should cut a new update - await delay(100); - pluginA$.next(available); - await delay(100); - subscription.unsubscribe(); - - expect(statusUpdates).toMatchInlineSnapshot(` - Array [ - Object { - "a": Object { - "level": degraded, - "summary": "a degraded", - }, - }, - Object { - "a": Object { - "level": available, - "summary": "a available", - }, - }, - ] - `); - }); - }); -}); diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts deleted file mode 100644 index df6f13eeec4e5..0000000000000 --- a/src/core/server/status/plugins_status.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs'; -import { map, distinctUntilChanged, switchMap, debounceTime } from 'rxjs/operators'; -import { isDeepStrictEqual } from 'util'; - -import { PluginName } from '../plugins'; -import { ServiceStatus, CoreStatus } from './types'; -import { getSummaryStatus } from './get_summary_status'; - -interface Deps { - core$: Observable; - pluginDependencies: ReadonlyMap; -} - -export class PluginsStatusService { - private readonly pluginStatuses = new Map>(); - private readonly update$ = new BehaviorSubject(true); - constructor(private readonly deps: Deps) {} - - public set(plugin: PluginName, status$: Observable) { - this.pluginStatuses.set(plugin, status$); - this.update$.next(true); // trigger all existing Observables to update from the new source Observable - } - - public getAll$(): Observable> { - return this.getPluginStatuses$([...this.deps.pluginDependencies.keys()]); - } - - public getDependenciesStatus$(plugin: PluginName): Observable> { - const dependencies = this.deps.pluginDependencies.get(plugin); - if (!dependencies) { - throw new Error(`Unknown plugin: ${plugin}`); - } - - return this.getPluginStatuses$(dependencies).pipe( - // Prevent many emissions at once from dependency status resolution from making this too noisy - debounceTime(100) - ); - } - - public getDerivedStatus$(plugin: PluginName): Observable { - return combineLatest([this.deps.core$, this.getDependenciesStatus$(plugin)]).pipe( - map(([coreStatus, pluginStatuses]) => { - return getSummaryStatus( - [...Object.entries(coreStatus), ...Object.entries(pluginStatuses)], - { - allAvailableSummary: `All dependencies are available`, - } - ); - }) - ); - } - - private getPluginStatuses$(plugins: PluginName[]): Observable> { - if (plugins.length === 0) { - return of({}); - } - - return this.update$.pipe( - switchMap(() => { - const pluginStatuses = plugins - .map( - (depName) => - [depName, this.pluginStatuses.get(depName) ?? this.getDerivedStatus$(depName)] as [ - PluginName, - Observable - ] - ) - .map(([pName, status$]) => - status$.pipe(map((status) => [pName, status] as [PluginName, ServiceStatus])) - ); - - return combineLatest(pluginStatuses).pipe( - map((statuses) => Object.fromEntries(statuses)), - distinctUntilChanged(isDeepStrictEqual) - ); - }) - ); - } -} diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index 42b3eecdca310..47ef8659b4079 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -40,9 +40,6 @@ const createSetupContractMock = () => { const setupContract: jest.Mocked = { core$: new BehaviorSubject(availableCoreStatus), overall$: new BehaviorSubject(available), - set: jest.fn(), - dependencies$: new BehaviorSubject({}), - derivedStatus$: new BehaviorSubject(available), }; return setupContract; @@ -53,11 +50,6 @@ const createInternalSetupContractMock = () => { core$: new BehaviorSubject(availableCoreStatus), overall$: new BehaviorSubject(available), isStatusPageAnonymous: jest.fn().mockReturnValue(false), - plugins: { - set: jest.fn(), - getDependenciesStatus$: jest.fn(), - getDerivedStatus$: jest.fn(), - }, }; return setupContract; diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index 341c40a86bf77..863fe34e8ecea 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -34,7 +34,6 @@ describe('StatusService', () => { service = new StatusService(mockCoreContext.create()); }); - const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const available: ServiceStatus = { level: ServiceStatusLevels.available, summary: 'Available', @@ -54,7 +53,6 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, - pluginDependencies: new Map(), }); expect(await setup.core$.pipe(first()).toPromise()).toEqual({ elasticsearch: available, @@ -70,7 +68,6 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, - pluginDependencies: new Map(), }); const subResult1 = await setup.core$.pipe(first()).toPromise(); const subResult2 = await setup.core$.pipe(first()).toPromise(); @@ -99,7 +96,6 @@ describe('StatusService', () => { savedObjects: { status$: savedObjects$, }, - pluginDependencies: new Map(), }); const statusUpdates: CoreStatus[] = []; @@ -162,7 +158,6 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, - pluginDependencies: new Map(), }); expect(await setup.overall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.degraded, @@ -178,7 +173,6 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, - pluginDependencies: new Map(), }); const subResult1 = await setup.overall$.pipe(first()).toPromise(); const subResult2 = await setup.overall$.pipe(first()).toPromise(); @@ -207,95 +201,26 @@ describe('StatusService', () => { savedObjects: { status$: savedObjects$, }, - pluginDependencies: new Map(), }); const statusUpdates: ServiceStatus[] = []; const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); - // Wait for timers to ensure that duplicate events are still filtered out regardless of debouncing. elasticsearch$.next(available); - await delay(100); elasticsearch$.next(available); - await delay(100); elasticsearch$.next({ level: ServiceStatusLevels.available, summary: `Wow another summary`, }); - await delay(100); savedObjects$.next(degraded); - await delay(100); savedObjects$.next(available); - await delay(100); savedObjects$.next(available); - await delay(100); subscription.unsubscribe(); expect(statusUpdates).toMatchInlineSnapshot(` Array [ Object { - "detail": "See the status page for more information", "level": degraded, - "meta": Object { - "affectedServices": Object { - "savedObjects": Object { - "level": degraded, - "summary": "This is degraded!", - }, - }, - }, - "summary": "[savedObjects]: This is degraded!", - }, - Object { - "level": available, - "summary": "All services are available", - }, - ] - `); - }); - - it('debounces events in quick succession', async () => { - const savedObjects$ = new BehaviorSubject(available); - const setup = await service.setup({ - elasticsearch: { - status$: new BehaviorSubject(available), - }, - savedObjects: { - status$: savedObjects$, - }, - pluginDependencies: new Map(), - }); - - const statusUpdates: ServiceStatus[] = []; - const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); - - // All of these should debounced into a single `available` status - savedObjects$.next(degraded); - savedObjects$.next(available); - savedObjects$.next(degraded); - savedObjects$.next(available); - savedObjects$.next(degraded); - savedObjects$.next(available); - savedObjects$.next(degraded); - // Waiting for the debounce timeout should cut a new update - await delay(100); - savedObjects$.next(available); - await delay(100); - subscription.unsubscribe(); - - expect(statusUpdates).toMatchInlineSnapshot(` - Array [ - Object { - "detail": "See the status page for more information", - "level": degraded, - "meta": Object { - "affectedServices": Object { - "savedObjects": Object { - "level": degraded, - "summary": "This is degraded!", - }, - }, - }, "summary": "[savedObjects]: This is degraded!", }, Object { diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index 59e81343597c9..aea335e64babf 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -18,7 +18,7 @@ */ import { Observable, combineLatest } from 'rxjs'; -import { map, distinctUntilChanged, shareReplay, take, debounceTime } from 'rxjs/operators'; +import { map, distinctUntilChanged, shareReplay, take } from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; import { CoreService } from '../../types'; @@ -26,16 +26,13 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { InternalElasticsearchServiceSetup } from '../elasticsearch'; import { InternalSavedObjectsServiceSetup } from '../saved_objects'; -import { PluginName } from '../plugins'; import { config, StatusConfigType } from './status_config'; import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; import { getSummaryStatus } from './get_summary_status'; -import { PluginsStatusService } from './plugins_status'; interface SetupDeps { elasticsearch: Pick; - pluginDependencies: ReadonlyMap; savedObjects: Pick; } @@ -43,29 +40,17 @@ export class StatusService implements CoreService { private readonly logger: Logger; private readonly config$: Observable; - private pluginsStatus?: PluginsStatusService; - constructor(coreContext: CoreContext) { this.logger = coreContext.logger.get('status'); this.config$ = coreContext.configService.atPath(config.path); } - public async setup({ elasticsearch, pluginDependencies, savedObjects }: SetupDeps) { + public async setup(core: SetupDeps) { const statusConfig = await this.config$.pipe(take(1)).toPromise(); - const core$ = this.setupCoreStatus({ elasticsearch, savedObjects }); - this.pluginsStatus = new PluginsStatusService({ core$, pluginDependencies }); - - const overall$: Observable = combineLatest( - core$, - this.pluginsStatus.getAll$() - ).pipe( - // Prevent many emissions at once from dependency status resolution from making this too noisy - debounceTime(100), - map(([coreStatus, pluginsStatus]) => { - const summary = getSummaryStatus([ - ...Object.entries(coreStatus), - ...Object.entries(pluginsStatus), - ]); + const core$ = this.setupCoreStatus(core); + const overall$: Observable = core$.pipe( + map((coreStatus) => { + const summary = getSummaryStatus(Object.entries(coreStatus)); this.logger.debug(`Recalculated overall status`, { status: summary }); return summary; }), @@ -75,11 +60,6 @@ export class StatusService implements CoreService { return { core$, overall$, - plugins: { - set: this.pluginsStatus.set.bind(this.pluginsStatus), - getDependenciesStatus$: this.pluginsStatus.getDependenciesStatus$.bind(this.pluginsStatus), - getDerivedStatus$: this.pluginsStatus.getDerivedStatus$.bind(this.pluginsStatus), - }, isStatusPageAnonymous: () => statusConfig.allowAnonymous, }; } @@ -88,10 +68,7 @@ export class StatusService implements CoreService { public stop() {} - private setupCoreStatus({ - elasticsearch, - savedObjects, - }: Pick): Observable { + private setupCoreStatus({ elasticsearch, savedObjects }: SetupDeps): Observable { return combineLatest([elasticsearch.status$, savedObjects.status$]).pipe( map(([elasticsearchStatus, savedObjectsStatus]) => ({ elasticsearch: elasticsearchStatus, diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts index f884b80316fa8..2ecf11deb2960 100644 --- a/src/core/server/status/types.ts +++ b/src/core/server/status/types.ts @@ -19,7 +19,6 @@ import { Observable } from 'rxjs'; import { deepFreeze } from '../../utils'; -import { PluginName } from '../plugins'; /** * The current status of a service at a point in time. @@ -117,60 +116,6 @@ export interface CoreStatus { /** * API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status. - * - * @remarks - * By default, a plugin inherits it's current status from the most severe status level of any Core services and any - * plugins that it depends on. This default status is available on the - * {@link ServiceStatusSetup.derivedStatus$ | core.status.derviedStatus$} API. - * - * Plugins may customize their status calculation by calling the {@link ServiceStatusSetup.set | core.status.set} API - * with an Observable. Within this Observable, a plugin may choose to only depend on the status of some of its - * dependencies, to ignore severe status levels of particular Core services they are not concerned with, or to make its - * status dependent on other external services. - * - * @example - * Customize a plugin's status to only depend on the status of SavedObjects: - * ```ts - * core.status.set( - * core.status.core$.pipe( - * . map((coreStatus) => { - * return coreStatus.savedObjects; - * }) ; - * ); - * ); - * ``` - * - * @example - * Customize a plugin's status to include an external service: - * ```ts - * const externalStatus$ = interval(1000).pipe( - * switchMap(async () => { - * const resp = await fetch(`https://myexternaldep.com/_healthz`); - * const body = await resp.json(); - * if (body.ok) { - * return of({ level: ServiceStatusLevels.available, summary: 'External Service is up'}); - * } else { - * return of({ level: ServiceStatusLevels.available, summary: 'External Service is unavailable'}); - * } - * }), - * catchError((error) => { - * of({ level: ServiceStatusLevels.unavailable, summary: `External Service is down`, meta: { error }}) - * }) - * ); - * - * core.status.set( - * combineLatest([core.status.derivedStatus$, externalStatus$]).pipe( - * map(([derivedStatus, externalStatus]) => { - * if (externalStatus.level > derivedStatus) { - * return externalStatus; - * } else { - * return derivedStatus; - * } - * }) - * ) - * ); - * ``` - * * @public */ export interface StatusServiceSetup { @@ -189,43 +134,9 @@ export interface StatusServiceSetup { * only depend on the statuses of {@link StatusServiceSetup.core$ | Core} or their dependencies. */ overall$: Observable; - - /** - * Allows a plugin to specify a custom status dependent on its own criteria. - * Completely overrides the default inherited status. - * - * @remarks - * See the {@link StatusServiceSetup.derivedStatus$} API for leveraging the default status - * calculation that is provided by Core. - */ - set(status$: Observable): void; - - /** - * Current status for all plugins this plugin depends on. - * Each key of the `Record` is a plugin id. - */ - dependencies$: Observable>; - - /** - * The status of this plugin as derived from its dependencies. - * - * @remarks - * By default, plugins inherit this derived status from their dependencies. - * Calling {@link StatusSetup.set} overrides this default status. - * - * This may emit multliple times for a single status change event as propagates - * through the dependency tree - */ - derivedStatus$: Observable; } /** @internal */ -export interface InternalStatusServiceSetup extends Pick { +export interface InternalStatusServiceSetup extends StatusServiceSetup { isStatusPageAnonymous: () => boolean; - // Namespaced under `plugins` key to improve clarity that these are APIs for plugins specifically. - plugins: { - set(plugin: PluginName, status$: Observable): void; - getDependenciesStatus$(plugin: PluginName): Observable>; - getDerivedStatus$(plugin: PluginName): Observable; - }; } diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts index d2e31dad58e55..61b71f8c5de07 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts @@ -24,7 +24,7 @@ import { TestElasticsearchUtils, TestKibanaUtils, TestUtils, -} from '../../../../../test_utils/kbn_server'; +} from '../../../../test_helpers/kbn_server'; import { createOrUpgradeSavedConfig } from '../create_or_upgrade_saved_config'; import { loggingSystemMock } from '../../../logging/logging_system.mock'; import { httpServerMock } from '../../../http/http_server.mocks'; diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index 04979b69b32b9..297deb0233c57 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -24,7 +24,7 @@ import { TestElasticsearchUtils, TestKibanaUtils, TestUtils, -} from '../../../../../test_utils/kbn_server'; +} from '../../../../test_helpers/kbn_server'; import { LegacyAPICaller } from '../../../elasticsearch/'; import { httpServerMock } from '../../../http/http_server.mocks'; diff --git a/src/core/server/ui_settings/integration_tests/routes.test.ts b/src/core/server/ui_settings/integration_tests/routes.test.ts index b18cc370fac3c..063d68e3866b7 100644 --- a/src/core/server/ui_settings/integration_tests/routes.test.ts +++ b/src/core/server/ui_settings/integration_tests/routes.test.ts @@ -18,7 +18,7 @@ */ import { schema } from '@kbn/config-schema'; -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../test_helpers/kbn_server'; describe('ui settings service', () => { describe('routes', () => { diff --git a/src/core/server/utils/index.ts b/src/core/server/utils/index.ts index b01a4c4e04899..d9c4217c4117f 100644 --- a/src/core/server/utils/index.ts +++ b/src/core/server/utils/index.ts @@ -20,3 +20,4 @@ export * from './crypto'; export * from './from_root'; export * from './package_json'; +export * from './streams'; diff --git a/src/core/server/utils/streams/concat_stream.test.ts b/src/core/server/utils/streams/concat_stream.test.ts new file mode 100644 index 0000000000000..e964ab2a7a97e --- /dev/null +++ b/src/core/server/utils/streams/concat_stream.test.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createListStream, createPromiseFromStreams, createConcatStream } from './index'; + +describe('concatStream', () => { + test('accepts an initial value', async () => { + const output = await createPromiseFromStreams([ + createListStream([1, 2, 3]), + createConcatStream([0]), + ]); + + expect(output).toEqual([0, 1, 2, 3]); + }); + + describe(`combines using the previous value's concat method`, () => { + test('works with strings', async () => { + const output = await createPromiseFromStreams([ + createListStream(['a', 'b', 'c']), + createConcatStream(), + ]); + expect(output).toEqual('abc'); + }); + + test('works with arrays', async () => { + const output = await createPromiseFromStreams([ + createListStream([[1], [2, 3, 4], [10]]), + createConcatStream(), + ]); + expect(output).toEqual([1, 2, 3, 4, 10]); + }); + + test('works with a mixture, starting with array', async () => { + const output = await createPromiseFromStreams([ + createListStream([[], 1, 2, 3, 4, [5, 6, 7]]), + createConcatStream(), + ]); + expect(output).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + test('fails when the value does not have a concat method', async () => { + let promise; + try { + promise = createPromiseFromStreams([createListStream([1, '1']), createConcatStream()]); + } catch (err) { + throw new Error('createPromiseFromStreams() should not fail synchronously'); + } + + try { + await promise; + throw new Error('Promise should have rejected'); + } catch (err) { + expect(err).toBeInstanceOf(Error); + expect(err.message).toContain('concat'); + } + }); + }); +}); diff --git a/src/legacy/utils/streams/concat_stream.js b/src/core/server/utils/streams/concat_stream.ts similarity index 96% rename from src/legacy/utils/streams/concat_stream.js rename to src/core/server/utils/streams/concat_stream.ts index e3f8f7261d2b7..03450cb51b832 100644 --- a/src/legacy/utils/streams/concat_stream.js +++ b/src/core/server/utils/streams/concat_stream.ts @@ -41,6 +41,6 @@ import { createReduceStream } from './reduce_stream'; * items will concat with * @return {Transform} */ -export function createConcatStream(initial) { +export function createConcatStream(initial?: T) { return createReduceStream((acc, chunk) => acc.concat(chunk), initial); } diff --git a/src/core/server/utils/streams/concat_stream_providers.test.ts b/src/core/server/utils/streams/concat_stream_providers.test.ts new file mode 100644 index 0000000000000..b742a770b70c8 --- /dev/null +++ b/src/core/server/utils/streams/concat_stream_providers.test.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Readable } from 'stream'; + +import { concatStreamProviders } from './concat_stream_providers'; +import { createListStream } from './list_stream'; +import { createConcatStream } from './concat_stream'; +import { createPromiseFromStreams } from './promise_from_streams'; + +describe('concatStreamProviders() helper', () => { + test('writes the data from an array of stream providers into a destination stream in order', async () => { + const results = await createPromiseFromStreams([ + concatStreamProviders([ + () => createListStream(['foo', 'bar']), + () => createListStream(['baz']), + () => createListStream(['bug']), + ]), + createConcatStream(''), + ]); + + expect(results).toBe('foobarbazbug'); + }); + + test('emits the errors from a sub-stream to the destination', async () => { + const dest = concatStreamProviders([ + () => createListStream(['foo', 'bar']), + () => + new Readable({ + read() { + this.emit('error', new Error('foo')); + }, + }), + ]); + + const errorListener = jest.fn(); + dest.on('error', errorListener); + + await expect(createPromiseFromStreams([dest])).rejects.toThrowErrorMatchingInlineSnapshot( + `"foo"` + ); + expect(errorListener.mock.calls).toMatchInlineSnapshot(` +Array [ + Array [ + [Error: foo], + ], +] +`); + }); +}); diff --git a/src/legacy/utils/streams/concat_stream_providers.js b/src/core/server/utils/streams/concat_stream_providers.ts similarity index 91% rename from src/legacy/utils/streams/concat_stream_providers.js rename to src/core/server/utils/streams/concat_stream_providers.ts index 11dfb84284df3..bb836e3d73787 100644 --- a/src/legacy/utils/streams/concat_stream_providers.js +++ b/src/core/server/utils/streams/concat_stream_providers.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PassThrough } from 'stream'; +import { Readable, PassThrough, TransformOptions } from 'stream'; /** * Write the data and errors from a list of stream providers @@ -29,7 +29,10 @@ import { PassThrough } from 'stream'; * @param {PassThroughOptions} options options passed to the PassThrough constructor * @return {WritableStream} combined stream */ -export function concatStreamProviders(sourceProviders, options = {}) { +export function concatStreamProviders( + sourceProviders: Array<() => Readable>, + options?: TransformOptions +) { const destination = new PassThrough(options); const queue = sourceProviders.slice(); diff --git a/src/core/server/utils/streams/filter_stream.test.ts b/src/core/server/utils/streams/filter_stream.test.ts new file mode 100644 index 0000000000000..41073e54b0a84 --- /dev/null +++ b/src/core/server/utils/streams/filter_stream.test.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + createConcatStream, + createFilterStream, + createListStream, + createPromiseFromStreams, +} from './index'; + +describe('createFilterStream()', () => { + test('calls the function with each item in the source stream', async () => { + const filter = jest.fn().mockReturnValue(true); + + await createPromiseFromStreams([createListStream(['a', 'b', 'c']), createFilterStream(filter)]); + + expect(filter).toMatchInlineSnapshot(` + [MockFunction] { + "calls": Array [ + Array [ + "a", + ], + Array [ + "b", + ], + Array [ + "c", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": true, + }, + Object { + "type": "return", + "value": true, + }, + Object { + "type": "return", + "value": true, + }, + ], + } + `); + }); + + test('send the filtered values on the output stream', async () => { + const result = await createPromiseFromStreams([ + createListStream([1, 2, 3]), + createFilterStream((n) => n % 2 === 0), + createConcatStream([]), + ]); + + expect(result).toMatchInlineSnapshot(` + Array [ + 2, + ] + `); + }); +}); diff --git a/src/legacy/utils/streams/filter_stream.ts b/src/core/server/utils/streams/filter_stream.ts similarity index 100% rename from src/legacy/utils/streams/filter_stream.ts rename to src/core/server/utils/streams/filter_stream.ts diff --git a/src/plugins/data/public/search/legacy/es_client/index.ts b/src/core/server/utils/streams/index.ts similarity index 58% rename from src/plugins/data/public/search/legacy/es_client/index.ts rename to src/core/server/utils/streams/index.ts index 78ac83af642d8..447d1ed5b1c53 100644 --- a/src/plugins/data/public/search/legacy/es_client/index.ts +++ b/src/core/server/utils/streams/index.ts @@ -17,5 +17,13 @@ * under the License. */ -export { getEsClient } from './get_es_client'; -export { LegacyApiCaller } from './types'; +export { concatStreamProviders } from './concat_stream_providers'; +export { createIntersperseStream } from './intersperse_stream'; +export { createSplitStream } from './split_stream'; +export { createListStream } from './list_stream'; +export { createReduceStream } from './reduce_stream'; +export { createPromiseFromStreams } from './promise_from_streams'; +export { createConcatStream } from './concat_stream'; +export { createMapStream } from './map_stream'; +export { createReplaceStream } from './replace_stream'; +export { createFilterStream } from './filter_stream'; diff --git a/src/core/server/utils/streams/intersperse_stream.test.ts b/src/core/server/utils/streams/intersperse_stream.test.ts new file mode 100644 index 0000000000000..9aa15035d2a1c --- /dev/null +++ b/src/core/server/utils/streams/intersperse_stream.test.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + createPromiseFromStreams, + createListStream, + createIntersperseStream, + createConcatStream, +} from './index'; + +describe('intersperseStream', () => { + test('places the intersperse value between each provided value', async () => { + expect( + await createPromiseFromStreams([ + createListStream(['to', 'be', 'or', 'not', 'to', 'be']), + createIntersperseStream(' '), + createConcatStream(), + ]) + ).toBe('to be or not to be'); + }); + + test('emits values as soon as possible, does not needlessly buffer', async () => { + const str = createIntersperseStream('y'); + const onData = jest.fn(); + str.on('data', onData); + + str.write('a'); + expect(onData).toHaveBeenCalledTimes(1); + expect(onData.mock.calls[0]).toEqual(['a']); + onData.mockClear(); + + str.write('b'); + expect(onData).toHaveBeenCalledTimes(2); + expect(onData.mock.calls[0]).toEqual(['y']); + expect(onData).toHaveBeenCalledTimes(2); + expect(onData.mock.calls[1]).toEqual(['b']); + }); +}); diff --git a/src/legacy/utils/streams/intersperse_stream.js b/src/core/server/utils/streams/intersperse_stream.ts similarity index 95% rename from src/legacy/utils/streams/intersperse_stream.js rename to src/core/server/utils/streams/intersperse_stream.ts index 5f9f0b03cd7eb..272507221caff 100644 --- a/src/legacy/utils/streams/intersperse_stream.js +++ b/src/core/server/utils/streams/intersperse_stream.ts @@ -40,7 +40,7 @@ import { Transform } from 'stream'; * @param {String|Buffer} intersperseChunk * @return {Transform} */ -export function createIntersperseStream(intersperseChunk) { +export function createIntersperseStream(intersperseChunk: string | Buffer) { let first = true; return new Transform({ @@ -55,7 +55,7 @@ export function createIntersperseStream(intersperseChunk) { } this.push(chunk); - callback(null); + callback(); } catch (err) { callback(err); } diff --git a/src/legacy/core_plugins/kibana/index.js b/src/core/server/utils/streams/list_stream.test.ts similarity index 50% rename from src/legacy/core_plugins/kibana/index.js rename to src/core/server/utils/streams/list_stream.test.ts index 722d75d00f78f..2a20c929db6b9 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/core/server/utils/streams/list_stream.test.ts @@ -17,23 +17,28 @@ * under the License. */ -import { getUiSettingDefaults } from './server/ui_setting_defaults'; +import { createListStream } from './index'; -export default function (kibana) { - return new kibana.Plugin({ - id: 'kibana', - config: function (Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - index: Joi.string().default('.kibana'), - autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000), - // TODO Also allow units here like in elasticsearch config once this is moved to the new platform - autocompleteTimeout: Joi.number().integer().min(1).default(1000), - }).default(); - }, +describe('listStream', () => { + test('provides the values in the initial list', async () => { + const str = createListStream([1, 2, 3, 4]); + const onData = jest.fn(); + str.on('data', onData); - uiExports: { - uiSettingDefaults: getUiSettingDefaults(), - }, + await new Promise((resolve) => str.on('end', resolve)); + + expect(onData).toHaveBeenCalledTimes(4); + expect(onData.mock.calls[0]).toEqual([1]); + expect(onData.mock.calls[1]).toEqual([2]); + expect(onData.mock.calls[2]).toEqual([3]); + expect(onData.mock.calls[3]).toEqual([4]); + }); + + test('does not modify the list passed', async () => { + const list = [1, 2, 3, 4]; + const str = createListStream(list); + str.resume(); + await new Promise((resolve) => str.on('end', resolve)); + expect(list).toEqual([1, 2, 3, 4]); }); -} +}); diff --git a/src/legacy/utils/streams/list_stream.js b/src/core/server/utils/streams/list_stream.ts similarity index 90% rename from src/legacy/utils/streams/list_stream.js rename to src/core/server/utils/streams/list_stream.ts index a614620b054b7..e62f6d3fa930b 100644 --- a/src/legacy/utils/streams/list_stream.js +++ b/src/core/server/utils/streams/list_stream.ts @@ -26,8 +26,8 @@ import { Readable } from 'stream'; * @param {Array} items - the list of items to provide * @return {Readable} */ -export function createListStream(items = []) { - const queue = [].concat(items); +export function createListStream(items: T | T[] = []) { + const queue = Array.isArray(items) ? [...items] : [items]; return new Readable({ objectMode: true, diff --git a/src/core/server/utils/streams/map_stream.test.ts b/src/core/server/utils/streams/map_stream.test.ts new file mode 100644 index 0000000000000..bf0cab39c21f4 --- /dev/null +++ b/src/core/server/utils/streams/map_stream.test.ts @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { delay } from 'bluebird'; + +import { createPromiseFromStreams } from './promise_from_streams'; +import { createListStream } from './list_stream'; +import { createMapStream } from './map_stream'; +import { createConcatStream } from './concat_stream'; + +describe('createMapStream()', () => { + test('calls the function with each item in the source stream', async () => { + const mapper = jest.fn(); + + await createPromiseFromStreams([createListStream(['a', 'b', 'c']), createMapStream(mapper)]); + + expect(mapper).toHaveBeenCalledTimes(3); + expect(mapper).toHaveBeenCalledWith('a', 0); + expect(mapper).toHaveBeenCalledWith('b', 1); + expect(mapper).toHaveBeenCalledWith('c', 2); + }); + + test('send the return value from the mapper on the output stream', async () => { + const result = await createPromiseFromStreams([ + createListStream([1, 2, 3]), + createMapStream((n: number) => n * 100), + createConcatStream([]), + ]); + + expect(result).toEqual([100, 200, 300]); + }); + + test('supports async mappers', async () => { + const result = await createPromiseFromStreams([ + createListStream([1, 2, 3]), + createMapStream(async (n: number, i: number) => { + await delay(n); + return n * i; + }), + createConcatStream([]), + ]); + + expect(result).toEqual([0, 2, 6]); + }); +}); diff --git a/src/legacy/utils/streams/map_stream.js b/src/core/server/utils/streams/map_stream.ts similarity index 93% rename from src/legacy/utils/streams/map_stream.js rename to src/core/server/utils/streams/map_stream.ts index 4e906471330f1..aad53cc526626 100644 --- a/src/legacy/utils/streams/map_stream.js +++ b/src/core/server/utils/streams/map_stream.ts @@ -19,7 +19,7 @@ import { Transform } from 'stream'; -export function createMapStream(fn) { +export function createMapStream(fn: (value: T, i: number) => void) { let i = 0; return new Transform({ diff --git a/src/core/server/utils/streams/promise_from_streams.test.ts b/src/core/server/utils/streams/promise_from_streams.test.ts new file mode 100644 index 0000000000000..1f2596c16a6fa --- /dev/null +++ b/src/core/server/utils/streams/promise_from_streams.test.ts @@ -0,0 +1,135 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Readable, Writable, Duplex, Transform } from 'stream'; + +import { createListStream, createPromiseFromStreams, createReduceStream } from './index'; + +describe('promiseFromStreams', () => { + test('pipes together an array of streams', async () => { + const str1 = createListStream([1, 2, 3]); + const str2 = createReduceStream((acc, n) => acc + n, 0); + const sumPromise = new Promise((resolve) => str2.once('data', resolve)); + createPromiseFromStreams([str1, str2]); + await new Promise((resolve) => str2.once('end', resolve)); + expect(await sumPromise).toBe(6); + }); + + describe('last stream is writable', () => { + test('waits for the last stream to finish writing', async () => { + let written = ''; + + await createPromiseFromStreams([ + createListStream(['a']), + new Writable({ + write(chunk, enc, cb) { + setTimeout(() => { + written += chunk; + cb(); + }, 100); + }, + }), + ]); + + expect(written).toBe('a'); + }); + + test('resolves to undefined', async () => { + const result = await createPromiseFromStreams([ + createListStream(['a']), + new Writable({ + write(chunk, enc, cb) { + cb(); + }, + }), + ]); + + expect(result).toBe(undefined); + }); + }); + + describe('last stream is readable', () => { + test(`resolves to it's final value`, async () => { + const result = await createPromiseFromStreams([createListStream(['a', 'b', 'c'])]); + + expect(result).toBe('c'); + }); + }); + + describe('last stream is duplex', () => { + test('waits for writing and resolves to final value', async () => { + let written = ''; + + const duplexReadQueue: Array> = []; + const duplexItemsToPush = ['foo', 'bar', null]; + const result = await createPromiseFromStreams([ + createListStream(['a', 'b', 'c']), + new Duplex({ + async read() { + this.push(await duplexReadQueue.shift()); + }, + + write(chunk, enc, cb) { + duplexReadQueue.push( + new Promise((resolve) => { + setTimeout(() => { + written += chunk; + cb(); + resolve(duplexItemsToPush.shift()); + }, 50); + }) + ); + }, + }).setEncoding('utf8'), + ]); + + expect(written).toEqual('abc'); + expect(result).toBe('bar'); + }); + }); + + describe('error handling', () => { + test('read stream gets destroyed when transform stream fails', async () => { + let destroyCalled = false; + const readStream = new Readable({ + read() { + this.push('a'); + this.push('b'); + this.push('c'); + this.push(null); + }, + destroy() { + destroyCalled = true; + }, + }); + const transformStream = new Transform({ + transform(chunk, enc, done) { + done(new Error('Test error')); + }, + }); + try { + await createPromiseFromStreams([readStream, transformStream]); + throw new Error('Should fail'); + } catch (e) { + expect(e.message).toBe('Test error'); + expect(destroyCalled).toBe(true); + } + }); + }); +}); diff --git a/src/core/server/utils/streams/promise_from_streams.ts b/src/core/server/utils/streams/promise_from_streams.ts new file mode 100644 index 0000000000000..f5fc4af62bc83 --- /dev/null +++ b/src/core/server/utils/streams/promise_from_streams.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Take an array of streams, pipe the output + * from each one into the next, listening for + * errors from any of the streams, and then resolve + * the promise once the final stream has finished + * writing/reading. + * + * If the last stream is readable, it's final value + * will be provided as the promise value. + * + * Errors emitted from any stream will cause + * the promise to be rejected with that error. + * + * @param {Array} streams + * @return {Promise} + */ + +import { pipeline, Writable, Readable } from 'stream'; + +function isReadable(stream: Readable | Writable): stream is Readable { + return 'read' in stream && typeof stream.read === 'function'; +} + +export async function createPromiseFromStreams(streams: [Readable, ...Writable[]]): Promise { + let finalChunk: any; + const last = streams[streams.length - 1]; + if (!isReadable(last) && streams.length === 1) { + // For a nicer error than what stream.pipeline throws + throw new Error('A minimum of 2 streams is required when a non-readable stream is given'); + } + if (isReadable(last)) { + // We are pushing a writable stream to capture the last chunk + streams.push( + new Writable({ + // Use object mode even when "last" stream isn't. This allows to + // capture the last chunk as-is. + objectMode: true, + write(chunk, enc, done) { + finalChunk = chunk; + done(); + }, + }) + ); + } + + return new Promise((resolve, reject) => { + // @ts-expect-error 'pipeline' doesn't support variable length of arguments + pipeline(...streams, (err) => { + if (err) return reject(err); + resolve(finalChunk); + }); + }); +} diff --git a/src/core/server/utils/streams/reduce_stream.test.ts b/src/core/server/utils/streams/reduce_stream.test.ts new file mode 100644 index 0000000000000..e4a7dc1cef491 --- /dev/null +++ b/src/core/server/utils/streams/reduce_stream.test.ts @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Transform } from 'stream'; +import { createReduceStream, createPromiseFromStreams, createListStream } from './index'; + +const promiseFromEvent = (name: string, emitter: Transform) => + new Promise((resolve) => emitter.on(name, () => resolve(name))); + +describe('reduceStream', () => { + test('calls the reducer for each item provided', async () => { + const stub = jest.fn(); + await createPromiseFromStreams([ + createListStream([1, 2, 3]), + createReduceStream((val, chunk, enc) => { + stub(val, chunk, enc); + return chunk; + }, 0), + ]); + expect(stub).toHaveBeenCalledTimes(3); + expect(stub.mock.calls[0]).toEqual([0, 1, 'utf8']); + expect(stub.mock.calls[1]).toEqual([1, 2, 'utf8']); + expect(stub.mock.calls[2]).toEqual([2, 3, 'utf8']); + }); + + test('provides the return value of the last iteration of the reducer', async () => { + const result = await createPromiseFromStreams([ + createListStream('abcdefg'.split('')), + createReduceStream((acc) => acc + 1, 0), + ]); + expect(result).toBe(7); + }); + + test('emits an error if an iteration fails', async () => { + const reduce = createReduceStream((acc, i) => { + expect(i).toBe(1); + return acc; + }, 0); + const errorEvent = promiseFromEvent('error', reduce); + + reduce.write(1); + reduce.write(2); + reduce.resume(); + await errorEvent; + }); + + test('stops calling the reducer if an iteration fails, emits no data', async () => { + const reducer = jest.fn((acc, i) => { + if (i < 100) return acc + i; + else throw new Error(i); + }); + const reduce$ = createReduceStream(reducer, 0); + + const dataStub = jest.fn(); + const errorStub = jest.fn(); + reduce$.on('data', dataStub); + reduce$.on('error', errorStub); + const endEvent = promiseFromEvent('end', reduce$); + + reduce$.write(1); + reduce$.write(2); + reduce$.write(300); + reduce$.write(400); + reduce$.write(1000); + reduce$.end(); + + await endEvent; + expect(reducer).toHaveBeenCalledTimes(3); + expect(dataStub).toHaveBeenCalledTimes(0); + expect(errorStub).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/legacy/utils/streams/reduce_stream.js b/src/core/server/utils/streams/reduce_stream.ts similarity index 95% rename from src/legacy/utils/streams/reduce_stream.js rename to src/core/server/utils/streams/reduce_stream.ts index d66b0124d1dab..9129df096ad13 100644 --- a/src/legacy/utils/streams/reduce_stream.js +++ b/src/core/server/utils/streams/reduce_stream.ts @@ -32,7 +32,10 @@ import { Transform } from 'stream'; * initial value. * @return {Transform} */ -export function createReduceStream(reducer, initial) { +export function createReduceStream( + reducer: (value: any, chunk: T, enc: string) => T, + initial?: T +) { let i = -1; let value = initial; diff --git a/src/core/server/utils/streams/replace_stream.test.ts b/src/core/server/utils/streams/replace_stream.test.ts new file mode 100644 index 0000000000000..c9da42395fb85 --- /dev/null +++ b/src/core/server/utils/streams/replace_stream.test.ts @@ -0,0 +1,132 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Writable, Readable } from 'stream'; + +import { + createReplaceStream, + createConcatStream, + createPromiseFromStreams, + createListStream, + createMapStream, +} from './index'; + +async function concatToString(streams: [Readable, ...Writable[]]) { + return await createPromiseFromStreams([ + ...streams, + createMapStream((buff: Buffer) => buff.toString('utf8')), + createConcatStream(''), + ]); +} + +describe('replaceStream', () => { + test('produces buffers when it receives buffers', async () => { + const chunks = await createPromiseFromStreams([ + createListStream([Buffer.from('foo'), Buffer.from('bar')]), + createReplaceStream('o', '0'), + createConcatStream([]), + ]); + + chunks.forEach((chunk) => { + expect(chunk).toBeInstanceOf(Buffer); + }); + }); + + test('produces buffers when it receives strings', async () => { + const chunks = await createPromiseFromStreams([ + createListStream(['foo', 'bar']), + createReplaceStream('o', '0'), + createConcatStream([]), + ]); + + chunks.forEach((chunk) => { + expect(chunk).toBeInstanceOf(Buffer); + }); + }); + + test('expects toReplace to be a string', () => { + // @ts-expect-error + expect(() => createReplaceStream(Buffer.from('foo'))).toThrowError(/be a string/); + }); + + test('replaces multiple single-char instances in a single chunk', async () => { + expect( + await concatToString([ + createListStream([Buffer.from('f00 bar')]), + createReplaceStream('0', 'o'), + ]) + ).toBe('foo bar'); + }); + + test('replaces multiple single-char instances in multiple chunks', async () => { + expect( + await concatToString([ + createListStream([Buffer.from('f0'), Buffer.from('0 bar')]), + createReplaceStream('0', 'o'), + ]) + ).toBe('foo bar'); + }); + + test('replaces single multi-char instances in single chunks', async () => { + expect( + await concatToString([ + createListStream([Buffer.from('f0'), Buffer.from('0 bar')]), + createReplaceStream('0', 'o'), + ]) + ).toBe('foo bar'); + }); + + test('replaces multiple multi-char instances in single chunks', async () => { + expect( + await concatToString([ + createListStream([Buffer.from('foo ba'), Buffer.from('r b'), Buffer.from('az bar')]), + createReplaceStream('bar', '*'), + ]) + ).toBe('foo * baz *'); + }); + + test('replaces multi-char instance that stretches multiple chunks', async () => { + expect( + await concatToString([ + createListStream([ + Buffer.from('foo supe'), + Buffer.from('rcalifra'), + Buffer.from('gilistic'), + Buffer.from('expialid'), + Buffer.from('ocious bar'), + ]), + createReplaceStream('supercalifragilisticexpialidocious', '*'), + ]) + ).toBe('foo * bar'); + }); + + test('ignores missing multi-char instance', async () => { + expect( + await concatToString([ + createListStream([ + Buffer.from('foo supe'), + Buffer.from('rcalifra'), + Buffer.from('gili stic'), + Buffer.from('expialid'), + Buffer.from('ocious bar'), + ]), + createReplaceStream('supercalifragilisticexpialidocious', '*'), + ]) + ).toBe('foo supercalifragili sticexpialidocious bar'); + }); +}); diff --git a/src/legacy/utils/streams/replace_stream.js b/src/core/server/utils/streams/replace_stream.ts similarity index 96% rename from src/legacy/utils/streams/replace_stream.js rename to src/core/server/utils/streams/replace_stream.ts index 7309bd241fa52..05391bb3341c2 100644 --- a/src/legacy/utils/streams/replace_stream.js +++ b/src/core/server/utils/streams/replace_stream.ts @@ -19,7 +19,7 @@ import { Transform } from 'stream'; -export function createReplaceStream(toReplace, replacement) { +export function createReplaceStream(toReplace: string, replacement: string | Buffer) { if (typeof toReplace !== 'string') { throw new TypeError('toReplace must be a string'); } @@ -78,6 +78,7 @@ export function createReplaceStream(toReplace, replacement) { this.push(buffer); } + // @ts-expect-error buffer = null; callback(); }, diff --git a/src/core/server/utils/streams/split_stream.test.ts b/src/core/server/utils/streams/split_stream.test.ts new file mode 100644 index 0000000000000..f131bd0661e54 --- /dev/null +++ b/src/core/server/utils/streams/split_stream.test.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Transform } from 'stream'; +import { createSplitStream, createConcatStream, createPromiseFromStreams } from './index'; + +async function split(stream: Transform, input: Array) { + const concat = createConcatStream(); + concat.write([]); + stream.pipe(concat); + const output = createPromiseFromStreams([concat]); + + input.forEach((i: any) => { + stream.write(i); + }); + stream.end(); + + return await output; +} + +describe('splitStream', () => { + test('splits buffers, produces strings', async () => { + const output = await split(createSplitStream('&'), [Buffer.from('foo&bar')]); + expect(output).toEqual(['foo', 'bar']); + }); + + test('supports mixed input', async () => { + const output = await split(createSplitStream('&'), [Buffer.from('foo&b'), 'ar']); + expect(output).toEqual(['foo', 'bar']); + }); + + test('supports buffer split chunks', async () => { + const output = await split(createSplitStream(Buffer.from('&')), ['foo&b', 'ar']); + expect(output).toEqual(['foo', 'bar']); + }); + + test('splits provided values by a delimiter', async () => { + const output = await split(createSplitStream('&'), ['foo&b', 'ar']); + expect(output).toEqual(['foo', 'bar']); + }); + + test('handles multi-character delimiters', async () => { + const output = await split(createSplitStream('oo'), ['foo&b', 'ar']); + expect(output).toEqual(['f', '&bar']); + }); + + test('handles delimiters that span multiple chunks', async () => { + const output = await split(createSplitStream('ba'), ['foo&b', 'ar']); + expect(output).toEqual(['foo&', 'r']); + }); + + test('produces an empty chunk if the split char is at the end of the input', async () => { + const output = await split(createSplitStream('&bar'), ['foo&b', 'ar']); + expect(output).toEqual(['foo', '']); + }); +}); diff --git a/src/legacy/utils/streams/split_stream.js b/src/core/server/utils/streams/split_stream.ts similarity index 95% rename from src/legacy/utils/streams/split_stream.js rename to src/core/server/utils/streams/split_stream.ts index f55cbc7bd290d..ae820f60abbf6 100644 --- a/src/legacy/utils/streams/split_stream.js +++ b/src/core/server/utils/streams/split_stream.ts @@ -38,7 +38,7 @@ import { Transform } from 'stream'; * @param {String} splitChunk * @return {Transform} */ -export function createSplitStream(splitChunk) { +export function createSplitStream(splitChunk: string | Uint8Array) { let unsplitBuffer = Buffer.alloc(0); return new Transform({ @@ -55,7 +55,7 @@ export function createSplitStream(splitChunk) { } unsplitBuffer = toSplit; - callback(null); + callback(); } catch (err) { callback(err); } @@ -65,7 +65,7 @@ export function createSplitStream(splitChunk) { try { this.push(unsplitBuffer.toString('utf8')); - callback(null); + callback(); } catch (err) { callback(err); } diff --git a/src/test_utils/public/http_test_setup.ts b/src/core/test_helpers/http_test_setup.ts similarity index 85% rename from src/test_utils/public/http_test_setup.ts rename to src/core/test_helpers/http_test_setup.ts index 7c70f64887af1..50ea43fb22b5e 100644 --- a/src/test_utils/public/http_test_setup.ts +++ b/src/core/test_helpers/http_test_setup.ts @@ -17,9 +17,9 @@ * under the License. */ -import { HttpService } from '../../core/public/http'; -import { fatalErrorsServiceMock } from '../../core/public/fatal_errors/fatal_errors_service.mock'; -import { injectedMetadataServiceMock } from '../../core/public/injected_metadata/injected_metadata_service.mock'; +import { HttpService } from '../public/http'; +import { fatalErrorsServiceMock } from '../public/fatal_errors/fatal_errors_service.mock'; +import { injectedMetadataServiceMock } from '../public/injected_metadata/injected_metadata_service.mock'; export type SetupTap = ( injectedMetadata: ReturnType, diff --git a/src/test_utils/kbn_server.ts b/src/core/test_helpers/kbn_server.ts similarity index 96% rename from src/test_utils/kbn_server.ts rename to src/core/test_helpers/kbn_server.ts index e44ce0de403d9..a494c6aa31d6f 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -26,16 +26,16 @@ import { kibanaServerTestUser, kibanaTestUser, setupUsers, - // @ts-ignore: implicit any for JS file } from '@kbn/test'; import { defaultsDeep, get } from 'lodash'; import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; import supertest from 'supertest'; -import { LegacyAPICaller } from '../core/server'; -import { CliArgs, Env } from '../core/server/config'; -import { Root } from '../core/server/root'; -import KbnServer from '../legacy/server/kbn_server'; + +import { LegacyAPICaller } from '../server/elasticsearch'; +import { CliArgs, Env } from '../server/config'; +import { Root } from '../server/root'; +import KbnServer from '../../legacy/server/kbn_server'; export type HttpMethod = 'delete' | 'get' | 'head' | 'post' | 'put'; @@ -53,7 +53,7 @@ const DEFAULTS_SETTINGS = { }; const DEFAULT_SETTINGS_WITH_CORE_PLUGINS = { - plugins: { scanDirs: [resolve(__dirname, '../legacy/core_plugins')] }, + plugins: { scanDirs: [resolve(__dirname, '../../legacy/core_plugins')] }, elasticsearch: { hosts: [esTestConfig.getUrl()], username: kibanaServerTestUser.username, diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json new file mode 100644 index 0000000000000..1a9e6253bff70 --- /dev/null +++ b/src/core/tsconfig.json @@ -0,0 +1,23 @@ +// { +// "extends": "../../tsconfig.base.json", +// "compilerOptions": { +// // "composite": true, +// "outDir": "./target", +// "emitDeclarationOnly": true, +// "declaration": true, +// "declarationMap": true +// }, +// "include": [ +// "public", +// "server", +// "types", +// "test_helpers", +// "utils", +// "index.ts", +// "../../kibana.d.ts", +// "../../typings/**/*" +// ], +// "references": [ +// { "path": "../test_utils" } +// ] +// } diff --git a/src/dev/build/lib/watch_stdio_for_line.ts b/src/dev/build/lib/watch_stdio_for_line.ts index 2322d017abc61..3d7929ccfc33a 100644 --- a/src/dev/build/lib/watch_stdio_for_line.ts +++ b/src/dev/build/lib/watch_stdio_for_line.ts @@ -24,7 +24,7 @@ import { createPromiseFromStreams, createSplitStream, createMapStream, -} from '../../../legacy/utils/streams'; +} from '../../../core/server/utils'; // creates a stream that skips empty lines unless they are followed by // another line, preventing the empty lines produced by splitStream diff --git a/src/dev/notice/generate_notice_from_source.ts b/src/dev/notice/generate_notice_from_source.ts index 0bef5bc5f32d4..9f7eb9d9e1aa4 100644 --- a/src/dev/notice/generate_notice_from_source.ts +++ b/src/dev/notice/generate_notice_from_source.ts @@ -41,7 +41,7 @@ interface Options { * into the repository. */ export async function generateNoticeFromSource({ productName, directory, log }: Options) { - const globs = ['**/*.{js,less,css,ts}']; + const globs = ['**/*.{js,less,css,ts,tsx}']; const options = { cwd: directory, diff --git a/src/dev/run_i18n_integrate.ts b/src/dev/run_i18n_integrate.ts index 25c3ea32783aa..c0b2302c91c54 100644 --- a/src/dev/run_i18n_integrate.ts +++ b/src/dev/run_i18n_integrate.ts @@ -111,6 +111,7 @@ run( const reporter = new ErrorReporter(); const messages: Map = new Map(); await list.run({ messages, reporter }); + process.exitCode = 0; } catch (error) { process.exitCode = 1; if (error instanceof ErrorReporter) { @@ -120,6 +121,7 @@ run( log.error(error); } } + process.exit(); }, { flags: { diff --git a/src/plugins/data/public/search/legacy/es_client/types.ts b/src/dev/typescript/build_refs.ts similarity index 65% rename from src/plugins/data/public/search/legacy/es_client/types.ts rename to src/dev/typescript/build_refs.ts index 2d35188322a4e..cbb596c185f8b 100644 --- a/src/plugins/data/public/search/legacy/es_client/types.ts +++ b/src/dev/typescript/build_refs.ts @@ -17,14 +17,21 @@ * under the License. */ -import { SearchResponse } from 'elasticsearch'; -import { SearchRequest } from '../../fetch'; +import execa from 'execa'; +import { run, ToolingLog } from '@kbn/dev-utils'; -export interface LegacyApiCaller { - search: (searchRequest: SearchRequest) => LegacyApiCallerResponse; - msearch: (searchRequest: SearchRequest) => LegacyApiCallerResponse; +export async function buildRefs(log: ToolingLog) { + try { + log.info('Building TypeScript projects refs...'); + await execa(require.resolve('typescript/bin/tsc'), ['-b', 'tsconfig.refs.json']); + } catch (e) { + log.error(e); + process.exit(1); + } } -interface LegacyApiCallerResponse extends Promise> { - abort: () => void; +export async function runBuildRefs() { + run(async ({ log }) => { + await buildRefs(log); + }); } diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 065321e355256..e18c82b5b9e96 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -24,6 +24,7 @@ import { Project } from './project'; export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'tsconfig.json')), + new Project(resolve(REPO_ROOT, 'src/test_utils/tsconfig.json')), new Project(resolve(REPO_ROOT, 'test/tsconfig.json'), { name: 'kibana/test' }), new Project(resolve(REPO_ROOT, 'x-pack/tsconfig.json')), new Project(resolve(REPO_ROOT, 'x-pack/test/tsconfig.json'), { name: 'x-pack/test' }), diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts index 9eeaeb4da7042..e1fca23274a5a 100644 --- a/src/dev/typescript/run_type_check_cli.ts +++ b/src/dev/typescript/run_type_check_cli.ts @@ -24,8 +24,9 @@ import getopts from 'getopts'; import { execInProjects } from './exec_in_projects'; import { filterProjectsByFlag } from './projects'; +import { buildRefs } from './build_refs'; -export function runTypeCheckCli() { +export async function runTypeCheckCli() { const extraFlags: string[] = []; const opts = getopts(process.argv.slice(2), { boolean: ['skip-lib-check', 'help'], @@ -79,7 +80,16 @@ export function runTypeCheckCli() { process.exit(); } - const tscArgs = ['--noEmit', '--pretty', ...(opts['skip-lib-check'] ? ['--skipLibCheck'] : [])]; + await buildRefs(log); + + const tscArgs = [ + // composite project cannot be used with --noEmit + ...['--composite', 'false'], + ...['--emitDeclarationOnly', 'false'], + '--noEmit', + '--pretty', + ...(opts['skip-lib-check'] ? ['--skipLibCheck'] : []), + ]; const projects = filterProjectsByFlag(opts.project).filter((p) => !p.disableTypeCheck); if (!projects.length) { diff --git a/src/legacy/core_plugins/kibana/package.json b/src/legacy/core_plugins/kibana/package.json deleted file mode 100644 index 94db646611df0..0000000000000 --- a/src/legacy/core_plugins/kibana/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "kibana", - "version": "kibana", - "config": { - "@elastic/eslint-import-resolver-kibana": { - "projectRoot": false - } - } -} diff --git a/src/legacy/core_plugins/kibana/public/index.scss b/src/legacy/core_plugins/kibana/public/index.scss deleted file mode 100644 index 7de0c8fc15f94..0000000000000 --- a/src/legacy/core_plugins/kibana/public/index.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Elastic charts -@import '@elastic/charts/dist/theme'; -@import '@elastic/eui/src/themes/charts/theme'; - -// Public UI styles -@import 'src/legacy/ui/public/index'; - diff --git a/src/legacy/deprecation/__tests__/create_transform.js b/src/legacy/deprecation/__tests__/create_transform.js deleted file mode 100644 index d3838da5c3399..0000000000000 --- a/src/legacy/deprecation/__tests__/create_transform.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createTransform } from '../create_transform'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -describe('deprecation', function () { - describe('createTransform', function () { - it(`doesn't modify settings parameter`, function () { - const settings = { - original: true, - }; - const deprecations = [ - (settings) => { - settings.original = false; - }, - ]; - createTransform(deprecations)(settings); - expect(settings.original).to.be(true); - }); - - it('calls single deprecation in array', function () { - const deprecations = [sinon.spy()]; - createTransform(deprecations)({}); - expect(deprecations[0].calledOnce).to.be(true); - }); - - it('calls multiple deprecations in array', function () { - const deprecations = [sinon.spy(), sinon.spy()]; - createTransform(deprecations)({}); - expect(deprecations[0].calledOnce).to.be(true); - expect(deprecations[1].calledOnce).to.be(true); - }); - - it('passes log function to deprecation', function () { - const deprecation = sinon.spy(); - const log = function () {}; - createTransform([deprecation])({}, log); - expect(deprecation.args[0][1]).to.be(log); - }); - }); -}); diff --git a/src/legacy/deprecation/deprecations/__tests__/rename.js b/src/legacy/deprecation/deprecations/__tests__/rename.js deleted file mode 100644 index 47c6b3257ff69..0000000000000 --- a/src/legacy/deprecation/deprecations/__tests__/rename.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { rename } from '../rename'; -import sinon from 'sinon'; - -describe('deprecation/deprecations', function () { - describe('rename', function () { - it('should rename simple property', function () { - const value = 'value'; - const settings = { - before: value, - }; - - rename('before', 'after')(settings); - expect(settings.before).to.be(undefined); - expect(settings.after).to.be(value); - }); - - it('should rename nested property', function () { - const value = 'value'; - const settings = { - someObject: { - before: value, - }, - }; - - rename('someObject.before', 'someObject.after')(settings); - expect(settings.someObject.before).to.be(undefined); - expect(settings.someObject.after).to.be(value); - }); - - it('should rename property, even when the value is null', function () { - const value = null; - const settings = { - before: value, - }; - - rename('before', 'after')(settings); - expect(settings.before).to.be(undefined); - expect(settings.after).to.be(null); - }); - - it(`shouldn't log when a rename doesn't occur`, function () { - const settings = { - exists: true, - }; - - const log = sinon.spy(); - rename('doesntExist', 'alsoDoesntExist')(settings, log); - expect(log.called).to.be(false); - }); - - it('should log when a rename does occur', function () { - const settings = { - exists: true, - }; - - const log = sinon.spy(); - rename('exists', 'alsoExists')(settings, log); - - expect(log.calledOnce).to.be(true); - expect(log.args[0][0]).to.match(/exists.+deprecated/); - }); - }); -}); diff --git a/src/legacy/deprecation/deprecations/__tests__/unused.js b/src/legacy/deprecation/deprecations/__tests__/unused.js deleted file mode 100644 index 4907c2b166989..0000000000000 --- a/src/legacy/deprecation/deprecations/__tests__/unused.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { unused } from '../unused'; - -describe('deprecation/deprecations', function () { - describe('unused', function () { - it('should remove unused setting', function () { - const settings = { - old: true, - }; - - unused('old')(settings); - expect(settings.old).to.be(undefined); - }); - - it(`shouldn't remove used setting`, function () { - const value = 'value'; - const settings = { - new: value, - }; - - unused('old')(settings); - expect(settings.new).to.be(value); - }); - - it('should remove unused setting, even when null', function () { - const settings = { - old: null, - }; - - unused('old')(settings); - expect(settings.old).to.be(undefined); - }); - - it('should log when removing unused setting', function () { - const settings = { - old: true, - }; - - const log = sinon.spy(); - unused('old')(settings, log); - - expect(log.calledOnce).to.be(true); - expect(log.args[0][0]).to.match(/old.+deprecated/); - }); - - it(`shouldn't log when no setting is unused`, function () { - const settings = { - new: true, - }; - - const log = sinon.spy(); - unused('old')(settings, log); - expect(log.called).to.be(false); - }); - }); -}); diff --git a/src/legacy/deprecation/deprecations/index.js b/src/legacy/deprecation/deprecations/index.js deleted file mode 100644 index 527c99309ba80..0000000000000 --- a/src/legacy/deprecation/deprecations/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { rename } from './rename'; -export { unused } from './unused'; diff --git a/src/legacy/deprecation/deprecations/rename.js b/src/legacy/deprecation/deprecations/rename.js deleted file mode 100644 index c96b9146b4e2c..0000000000000 --- a/src/legacy/deprecation/deprecations/rename.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { set } from '@elastic/safer-lodash-set'; -import { get, isUndefined, noop } from 'lodash'; -import { unset } from '../../utils'; - -export function rename(oldKey, newKey) { - return (settings, log = noop) => { - const value = get(settings, oldKey); - if (isUndefined(value)) { - return; - } - - unset(settings, oldKey); - set(settings, newKey, value); - - log(`Config key "${oldKey}" is deprecated. It has been replaced with "${newKey}"`); - }; -} diff --git a/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js b/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js index a74bfb872e99c..40f84f6f54b3b 100644 --- a/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js +++ b/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js @@ -45,10 +45,6 @@ describe('plugin discovery/extend config service', () => { enabled: Joi.boolean().default(true), test: Joi.string().default('bonk'), }).default(), - - deprecations({ rename }) { - return [rename('oldTest', 'test')]; - }, }), }) .getPluginSpecs() @@ -74,16 +70,14 @@ describe('plugin discovery/extend config service', () => { getConfigPrefix: sandbox.stub().returns(configPrefix), }; - const logDeprecation = sandbox.stub(); - const getSettings = sandbox.stub(SettingsNS, 'getSettings').returns(rootSettings.foo.bar); const getSchema = sandbox.stub(SchemaNS, 'getSchema').returns(schema); - await extendConfigService(pluginSpec, config, rootSettings, logDeprecation); + await extendConfigService(pluginSpec, config, rootSettings); sinon.assert.calledOnce(getSettings); - sinon.assert.calledWithExactly(getSettings, pluginSpec, rootSettings, logDeprecation); + sinon.assert.calledWithExactly(getSettings, pluginSpec, rootSettings); sinon.assert.calledOnce(getSchema); sinon.assert.calledWithExactly(getSchema, pluginSpec); @@ -145,41 +139,6 @@ describe('plugin discovery/extend config service', () => { expect(error.message).to.contain('"test" must be a string'); } }); - - it('calls logDeprecation() with deprecation messages', async () => { - const config = Config.withDefaultSchema(); - const logDeprecation = sinon.stub(); - await extendConfigService( - pluginSpec, - config, - { - foo: { - bar: { - baz: { - oldTest: '123', - }, - }, - }, - }, - logDeprecation - ); - sinon.assert.calledOnce(logDeprecation); - sinon.assert.calledWithExactly(logDeprecation, sinon.match('"oldTest" is deprecated')); - }); - - it('uses settings after transforming deprecations', async () => { - const config = Config.withDefaultSchema(); - await extendConfigService(pluginSpec, config, { - foo: { - bar: { - baz: { - oldTest: '123', - }, - }, - }, - }); - expect(config.get('foo.bar.baz.test')).to.be('123'); - }); }); describe('disableConfigExtension()', () => { diff --git a/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js b/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js index 2a26e29dfd63a..750c5ee6c6f50 100644 --- a/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js +++ b/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js @@ -18,7 +18,6 @@ */ import expect from '@kbn/expect'; -import sinon from 'sinon'; import { PluginPack } from '../../plugin_pack'; import { getSettings } from '../settings'; @@ -33,7 +32,6 @@ describe('plugin_discovery/settings', () => { provider: ({ Plugin }) => new Plugin({ configPrefix: 'a.b.c', - deprecations: ({ rename }) => [rename('foo', 'bar')], }), }) .getPluginSpecs() @@ -59,28 +57,5 @@ describe('plugin_discovery/settings', () => { it('allows rootSettings to be undefined', async () => { expect(await getSettings(pluginSpec)).to.eql(undefined); }); - - it('resolves deprecations', async () => { - const logDeprecation = sinon.stub(); - expect( - await getSettings( - pluginSpec, - { - a: { - b: { - c: { - foo: true, - }, - }, - }, - }, - logDeprecation - ) - ).to.eql({ - bar: true, - }); - - sinon.assert.calledOnce(logDeprecation); - }); }); }); diff --git a/src/legacy/plugin_discovery/plugin_config/extend_config_service.js b/src/legacy/plugin_discovery/plugin_config/extend_config_service.js index 9257227c0d62b..a6d5d4ae5f990 100644 --- a/src/legacy/plugin_discovery/plugin_config/extend_config_service.js +++ b/src/legacy/plugin_discovery/plugin_config/extend_config_service.js @@ -30,8 +30,8 @@ import { getSchema, getStubSchema } from './schema'; * @param {Function} [logDeprecation] * @return {Promise} */ -export async function extendConfigService(spec, config, rootSettings, logDeprecation) { - const settings = await getSettings(spec, rootSettings, logDeprecation); +export async function extendConfigService(spec, config, rootSettings) { + const settings = await getSettings(spec, rootSettings); const schema = await getSchema(spec); config.extendSchema(schema, settings, spec.getConfigPrefix()); } diff --git a/src/legacy/plugin_discovery/plugin_config/settings.js b/src/legacy/plugin_discovery/plugin_config/settings.js index 44ecb5718fe21..e6a4741d76eca 100644 --- a/src/legacy/plugin_discovery/plugin_config/settings.js +++ b/src/legacy/plugin_discovery/plugin_config/settings.js @@ -19,20 +19,16 @@ import { get } from 'lodash'; -import { getTransform } from '../../deprecation'; - /** * Get the settings for a pluginSpec from the raw root settings while * optionally calling logDeprecation() with warnings about deprecated * settings that were used * @param {PluginSpec} spec * @param {Object} rootSettings - * @param {Function} [logDeprecation] * @return {Promise} */ -export async function getSettings(spec, rootSettings, logDeprecation) { +export async function getSettings(spec, rootSettings) { const prefix = spec.getConfigPrefix(); const rawSettings = get(rootSettings, prefix); - const transform = await getTransform(spec); - return transform(rawSettings, logDeprecation); + return rawSettings; } diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 6cd3f8dc448b0..dd65e45659ffc 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -231,6 +231,15 @@ export default () => locale: Joi.string().default('en'), }).default(), + // temporarily moved here from the (now deleted) kibana legacy plugin + kibana: Joi.object({ + enabled: Joi.boolean().default(true), + index: Joi.string().default('.kibana'), + autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000), + // TODO Also allow units here like in elasticsearch config once this is moved to the new platform + autocompleteTimeout: Joi.number().integer().min(1).default(1000), + }).default(), + savedObjects: Joi.object({ maxImportPayloadBytes: Joi.number().default(10485760), maxImportExportSize: Joi.number().default(10000), diff --git a/src/legacy/server/http/integration_tests/max_payload_size.test.js b/src/legacy/server/http/integration_tests/max_payload_size.test.js index 789a54f681ba6..2d0718dd35606 100644 --- a/src/legacy/server/http/integration_tests/max_payload_size.test.js +++ b/src/legacy/server/http/integration_tests/max_payload_size.test.js @@ -17,7 +17,7 @@ * under the License. */ -import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import * as kbnTestServer from '../../../../core/test_helpers/kbn_server'; let root; beforeAll(async () => { diff --git a/src/legacy/server/i18n/index.ts b/src/legacy/server/i18n/index.ts index 09f7022436049..e895f83fe6901 100644 --- a/src/legacy/server/i18n/index.ts +++ b/src/legacy/server/i18n/index.ts @@ -20,7 +20,6 @@ import { i18n, i18nLoader } from '@kbn/i18n'; import { basename } from 'path'; import { Server } from 'hapi'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { fromRoot } from '../../../core/server/utils'; import { getTranslationPaths } from './get_translations_path'; import { I18N_RC } from './constants'; diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 1a1f43b93f26e..69fb63fbbd87f 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -50,10 +50,6 @@ import { HomeServerPluginSetup } from '../../plugins/home/server'; // lot of legacy code was assuming this type only had these two methods export type KibanaConfig = Pick; -export interface UiApp { - getId(): string; -} - // Extend the defaults with the plugins and server methods we need. declare module 'hapi' { interface PluginProperties { @@ -66,17 +62,6 @@ declare module 'hapi' { interface Server { config: () => KibanaConfig; savedObjects: SavedObjectsLegacyService; - injectUiAppVars: (pluginName: string, getAppVars: () => { [key: string]: any }) => void; - getHiddenUiAppById(appId: string): UiApp; - addScopedTutorialContextFactory: ( - scopedTutorialContextFactory: (...args: any[]) => any - ) => void; - getInjectedUiAppVars: (pluginName: string) => { [key: string]: any }; - getUiNavLinks(): Array<{ _id: string }>; - addMemoizedFactoryToRequest: ( - name: string, - factoryFn: (request: Request) => Record - ) => void; logWithMetadata: (tags: string[], message: string, meta: Record) => void; newPlatform: KbnServer['newPlatform']; } @@ -86,10 +71,6 @@ declare module 'hapi' { getBasePath(): string; getUiSettingsService(): IUiSettingsClient; } - - interface ResponseToolkit { - renderAppWithDefaultConfig(app: UiApp): ResponseObject; - } } type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise | void; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 320086b6d6531..4692262d99bb5 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -34,7 +34,6 @@ import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; import * as Plugins from './plugins'; import { savedObjectsMixin } from './saved_objects/saved_objects_mixin'; -import { serverExtensionsMixin } from './server_extensions'; import { uiMixin } from '../ui'; import { i18nMixin } from './i18n'; @@ -91,8 +90,6 @@ export default class KbnServer { coreMixin, - // adds methods for extending this.server - serverExtensionsMixin, loggingMixin, warningsMixin, statusMixin, diff --git a/src/legacy/server/logging/log_format_json.test.js b/src/legacy/server/logging/log_format_json.test.js index 31e622ecae611..f4fb939750566 100644 --- a/src/legacy/server/logging/log_format_json.test.js +++ b/src/legacy/server/logging/log_format_json.test.js @@ -21,7 +21,7 @@ import moment from 'moment'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { attachMetaData } from '../../../../src/core/server/legacy/logging/legacy_logging_server'; -import { createListStream, createPromiseFromStreams } from '../../utils'; +import { createListStream, createPromiseFromStreams } from '../../../core/server/utils'; import KbnLoggerJsonFormat from './log_format_json'; diff --git a/src/legacy/server/logging/log_format_string.test.js b/src/legacy/server/logging/log_format_string.test.js index 067ad70380961..842325865cce2 100644 --- a/src/legacy/server/logging/log_format_string.test.js +++ b/src/legacy/server/logging/log_format_string.test.js @@ -21,7 +21,7 @@ import moment from 'moment'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { attachMetaData } from '../../../../src/core/server/legacy/logging/legacy_logging_server'; -import { createListStream, createPromiseFromStreams } from '../../utils'; +import { createListStream, createPromiseFromStreams } from '../../../core/server/utils'; import KbnLoggerStringFormat from './log_format_string'; diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index 185c8807ae8b5..96cf2058839cf 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -39,14 +39,6 @@ export function savedObjectsMixin(kbnServer, server) { server.decorate('server', 'kibanaMigrator', migrator); - const warn = (message) => server.log(['warning', 'saved-objects'], message); - // we use kibana.index which is technically defined in the kibana plugin, so if - // we don't have the plugin (mainly tests) we can't initialize the saved objects - if (!kbnServer.pluginSpecs.some((p) => p.getId() === 'kibana')) { - warn('Saved Objects uninitialized because the Kibana plugin is disabled.'); - return; - } - const serializer = kbnServer.newPlatform.start.core.savedObjects.createSerializer(); const createRepository = (callCluster, includedHiddenTypes = []) => { diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js index 63e4a632ab5e0..d1d6c052ad589 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js @@ -161,21 +161,6 @@ describe('Saved Objects Mixin', () => { }; }); - describe('no kibana plugin', () => { - it('should not try to create anything', () => { - mockKbnServer.pluginSpecs.some = () => false; - savedObjectsMixin(mockKbnServer, mockServer); - expect(mockServer.log).toHaveBeenCalledWith(expect.any(Array), expect.any(String)); - expect(mockServer.decorate).toHaveBeenCalledWith( - 'server', - 'kibanaMigrator', - expect.any(Object) - ); - expect(mockServer.decorate).toHaveBeenCalledTimes(1); - expect(mockServer.route).not.toHaveBeenCalled(); - }); - }); - describe('Saved object service', () => { let service; diff --git a/src/legacy/server/server_extensions/add_memoized_factory_to_request.test.js b/src/legacy/server/server_extensions/add_memoized_factory_to_request.test.js deleted file mode 100644 index 48bd082468061..0000000000000 --- a/src/legacy/server/server_extensions/add_memoized_factory_to_request.test.js +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; - -import { serverExtensionsMixin } from './server_extensions_mixin'; - -describe('server.addMemoizedFactoryToRequest()', () => { - const setup = () => { - class Request {} - - class Server { - constructor() { - sinon.spy(this, 'decorate'); - } - decorate(type, name, value) { - switch (type) { - case 'request': - return (Request.prototype[name] = value); - case 'server': - return (Server.prototype[name] = value); - default: - throw new Error(`Unexpected decorate type ${type}`); - } - } - } - - const server = new Server(); - serverExtensionsMixin({}, server); - return { server, Request }; - }; - - it('throws when propertyName is not a string', () => { - const { server } = setup(); - expect(() => server.addMemoizedFactoryToRequest()).toThrowError('methodName must be a string'); - expect(() => server.addMemoizedFactoryToRequest(null)).toThrowError( - 'methodName must be a string' - ); - expect(() => server.addMemoizedFactoryToRequest(1)).toThrowError('methodName must be a string'); - expect(() => server.addMemoizedFactoryToRequest(true)).toThrowError( - 'methodName must be a string' - ); - expect(() => server.addMemoizedFactoryToRequest(/abc/)).toThrowError( - 'methodName must be a string' - ); - expect(() => server.addMemoizedFactoryToRequest(['foo'])).toThrowError( - 'methodName must be a string' - ); - expect(() => server.addMemoizedFactoryToRequest([1])).toThrowError( - 'methodName must be a string' - ); - expect(() => server.addMemoizedFactoryToRequest({})).toThrowError( - 'methodName must be a string' - ); - }); - - it('throws when factory is not a function', () => { - const { server } = setup(); - expect(() => server.addMemoizedFactoryToRequest('name')).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', null)).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', 1)).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', true)).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', /abc/)).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', ['foo'])).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', [1])).toThrowError( - 'factory must be a function' - ); - expect(() => server.addMemoizedFactoryToRequest('name', {})).toThrowError( - 'factory must be a function' - ); - }); - - it('throws when factory takes more than one arg', () => { - const { server } = setup(); - /* eslint-disable no-unused-vars */ - expect(() => server.addMemoizedFactoryToRequest('name', () => {})).not.toThrowError( - 'more than one argument' - ); - expect(() => server.addMemoizedFactoryToRequest('name', (a) => {})).not.toThrowError( - 'more than one argument' - ); - expect(() => server.addMemoizedFactoryToRequest('name', (a, b) => {})).toThrowError( - 'more than one argument' - ); - expect(() => server.addMemoizedFactoryToRequest('name', (a, b, c) => {})).toThrowError( - 'more than one argument' - ); - expect(() => server.addMemoizedFactoryToRequest('name', (a, b, c, d) => {})).toThrowError( - 'more than one argument' - ); - expect(() => server.addMemoizedFactoryToRequest('name', (a, b, c, d, e) => {})).toThrowError( - 'more than one argument' - ); - /* eslint-enable no-unused-vars */ - }); - - it('decorates request objects with a function at `propertyName`', () => { - const { server, Request } = setup(); - - expect(new Request()).not.toHaveProperty('decorated'); - server.addMemoizedFactoryToRequest('decorated', () => {}); - expect(typeof new Request().decorated).toBe('function'); - }); - - it('caches invocations of the factory to the request instance', () => { - const { server, Request } = setup(); - const factory = sinon.stub().returnsArg(0); - server.addMemoizedFactoryToRequest('foo', factory); - - const request1 = new Request(); - const request2 = new Request(); - - // call `foo()` on both requests a bunch of times, each time - // the return value should be exactly the same - expect(request1.foo()).toBe(request1); - expect(request1.foo()).toBe(request1); - expect(request1.foo()).toBe(request1); - expect(request1.foo()).toBe(request1); - expect(request1.foo()).toBe(request1); - expect(request1.foo()).toBe(request1); - - expect(request2.foo()).toBe(request2); - expect(request2.foo()).toBe(request2); - expect(request2.foo()).toBe(request2); - expect(request2.foo()).toBe(request2); - - // only two different requests, so factory should have only been - // called twice with the two request objects - sinon.assert.calledTwice(factory); - sinon.assert.calledWithExactly(factory, request1); - sinon.assert.calledWithExactly(factory, request2); - }); -}); diff --git a/src/legacy/server/server_extensions/server_extensions_mixin.js b/src/legacy/server/server_extensions/server_extensions_mixin.js deleted file mode 100644 index 19c0b24ae15a1..0000000000000 --- a/src/legacy/server/server_extensions/server_extensions_mixin.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function serverExtensionsMixin(kbnServer, server) { - /** - * Decorate all request objects with a new method, `methodName`, - * that will call the `factory` on first invocation and return - * the result of the first call to subsequent invocations. - * - * @method server.addMemoizedFactoryToRequest - * @param {string} methodName location on the request this - * factory should be added - * @param {Function} factory the factory to add to the request, - * which will be called once per request - * with a single argument, the request. - * @return {undefined} - */ - server.decorate('server', 'addMemoizedFactoryToRequest', (methodName, factory) => { - if (typeof methodName !== 'string') { - throw new TypeError('methodName must be a string'); - } - - if (typeof factory !== 'function') { - throw new TypeError('factory must be a function'); - } - - if (factory.length > 1) { - throw new TypeError(` - factory must not take more than one argument, the request object. - Memoization is done based on the request instance and is cached and reused - regardless of other arguments. If you want to have a per-request cache that - also does some sort of secondary memoization then return an object or function - from the memoized decorator and do secondary memoization there. - `); - } - - const requestCache = new WeakMap(); - server.decorate('request', methodName, function () { - const request = this; - - if (!requestCache.has(request)) { - requestCache.set(request, factory(request)); - } - - return requestCache.get(request); - }); - }); -} diff --git a/src/legacy/ui/ui_apps/__snapshots__/ui_apps_mixin.test.js.snap b/src/legacy/ui/ui_apps/__snapshots__/ui_apps_mixin.test.js.snap deleted file mode 100644 index cb2b6cda26a81..0000000000000 --- a/src/legacy/ui/ui_apps/__snapshots__/ui_apps_mixin.test.js.snap +++ /dev/null @@ -1,92 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UiAppsMixin creates kbnServer.uiApps from uiExports 1`] = ` -Array [ - StubUiApp { - "_hidden": true, - "_id": "foo", - }, - StubUiApp { - "_hidden": false, - "_id": "bar", - }, -] -`; - -exports[`UiAppsMixin decorates server with methods 1`] = ` -Array [ - Array [ - "server", - "getAllUiApps", - [Function], - ], - Array [ - "server", - "getUiAppById", - [Function], - ], - Array [ - "server", - "getHiddenUiAppById", - [Function], - ], - Array [ - "server", - "injectUiAppVars", - [Function], - ], - Array [ - "server", - "getInjectedUiAppVars", - [Function], - ], -] -`; - -exports[`UiAppsMixin server.getAllUiApps() returns hidden and non-hidden apps 1`] = ` -Array [ - StubUiApp { - "_hidden": true, - "_id": "foo", - }, - StubUiApp { - "_hidden": false, - "_id": "bar", - }, -] -`; - -exports[`UiAppsMixin server.getHiddenUiAppById() returns hidden apps when requested, undefined for non-hidden and unknown apps 1`] = ` -StubUiApp { - "_hidden": true, - "_id": "foo", -} -`; - -exports[`UiAppsMixin server.getUiAppById() returns non-hidden apps when requested, undefined for non-hidden and unknown apps 1`] = ` -StubUiApp { - "_hidden": false, - "_id": "bar", -} -`; - -exports[`UiAppsMixin server.injectUiAppVars()/server.getInjectedUiAppVars() merges injected vars provided by multiple providers in the order they are registered: foo 1`] = ` -Object { - "bar": false, - "baz": 1, - "box": true, - "foo": true, -} -`; - -exports[`UiAppsMixin server.injectUiAppVars()/server.getInjectedUiAppVars() stored injectVars provider and returns provider result when requested: bar 1`] = ` -Object { - "thisIsFoo": false, -} -`; - -exports[`UiAppsMixin server.injectUiAppVars()/server.getInjectedUiAppVars() stored injectVars provider and returns provider result when requested: foo 1`] = ` -Object { - "thisIsFoo": true, -} -`; diff --git a/src/legacy/ui/ui_apps/__tests__/ui_app.js b/src/legacy/ui/ui_apps/__tests__/ui_app.js deleted file mode 100644 index bb4bcfe2d7443..0000000000000 --- a/src/legacy/ui/ui_apps/__tests__/ui_app.js +++ /dev/null @@ -1,306 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; -import expect from '@kbn/expect'; - -import { UiApp } from '../ui_app'; -import { UiNavLink } from '../../ui_nav_links'; - -function createStubUiAppSpec(extraParams) { - return { - id: 'uiapp-test', - main: 'main.js', - title: 'UIApp Test', - order: 9000, - icon: 'ui_app_test.svg', - linkToLastSubUrl: true, - hidden: false, - listed: false, - ...extraParams, - }; -} - -function createStubKbnServer(extraParams) { - return { - plugins: [], - config: { - get: sinon.stub().withArgs('server.basePath').returns(''), - }, - server: {}, - ...extraParams, - }; -} - -function createUiApp(spec = createStubUiAppSpec(), kbnServer = createStubKbnServer()) { - return new UiApp(kbnServer, spec); -} - -describe('ui apps / UiApp', () => { - describe('constructor', () => { - it('throws an exception if an ID is not given', () => { - const spec = {}; // should have id property - expect(() => createUiApp(spec)).to.throwException(); - }); - - describe('defaults', () => { - const spec = { id: 'uiapp-test-defaults' }; - const app = createUiApp(spec); - - it('has the ID from the spec', () => { - expect(app.getId()).to.be(spec.id); - }); - - it('has no plugin ID', () => { - expect(app.getPluginId()).to.be(undefined); - }); - - it('is not hidden', () => { - expect(app.isHidden()).to.be(false); - }); - - it('is listed', () => { - expect(app.isListed()).to.be(true); - }); - - it('has a navLink', () => { - expect(app.getNavLink()).to.be.a(UiNavLink); - }); - - it('has no main module', () => { - expect(app.getMainModuleId()).to.be(undefined); - }); - - it('has a mostly empty JSON representation', () => { - expect(JSON.parse(JSON.stringify(app))).to.eql({ - id: spec.id, - navLink: { - id: 'uiapp-test-defaults', - order: 0, - url: '/app/uiapp-test-defaults', - subUrlBase: '/app/uiapp-test-defaults', - linkToLastSubUrl: true, - hidden: false, - disabled: false, - tooltip: '', - }, - }); - }); - }); - - describe('mock spec', () => { - const spec = createStubUiAppSpec(); - const app = createUiApp(spec); - - it('has the ID from the spec', () => { - expect(app.getId()).to.be(spec.id); - }); - - it('has no plugin ID', () => { - expect(app.getPluginId()).to.be(undefined); - }); - - it('is not hidden', () => { - expect(app.isHidden()).to.be(false); - }); - - it('is also not listed', () => { - expect(app.isListed()).to.be(false); - }); - - it('has no navLink', () => { - expect(app.getNavLink()).to.be(undefined); - }); - - it('has a main module', () => { - expect(app.getMainModuleId()).to.be('main.js'); - }); - - it('has spec values in JSON representation', () => { - expect(JSON.parse(JSON.stringify(app))).to.eql({ - id: spec.id, - title: spec.title, - icon: spec.icon, - main: spec.main, - linkToLastSubUrl: spec.linkToLastSubUrl, - navLink: { - id: 'uiapp-test', - title: 'UIApp Test', - order: 9000, - url: '/app/uiapp-test', - subUrlBase: '/app/uiapp-test', - icon: 'ui_app_test.svg', - linkToLastSubUrl: true, - hidden: false, - disabled: false, - tooltip: '', - }, - }); - }); - }); - - /* - * The "hidden" and "listed" flags have an bound relationship. The "hidden" - * flag gets cast to a boolean value, and the "listed" flag is dependent on - * "hidden" - */ - describe('hidden flag', () => { - describe('is cast to boolean value', () => { - it('when undefined', () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isHidden()).to.be(false); - }); - - it('when null', () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: null, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isHidden()).to.be(false); - }); - - it('when 0', () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: 0, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isHidden()).to.be(false); - }); - - it('when true', () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: true, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isHidden()).to.be(true); - }); - - it('when 1', () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: 1, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isHidden()).to.be(true); - }); - }); - }); - - describe('listed flag', () => { - describe('defaults to the opposite value of hidden', () => { - it(`when it's null and hidden is true`, () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: true, - listed: null, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isListed()).to.be(false); - }); - - it(`when it's null and hidden is false`, () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: false, - listed: null, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isListed()).to.be(true); - }); - - it(`when it's undefined and hidden is false`, () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: false, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isListed()).to.be(true); - }); - - it(`when it's undefined and hidden is true`, () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: true, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isListed()).to.be(false); - }); - }); - - it(`is set to true when it's passed as true`, () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - listed: true, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isListed()).to.be(true); - }); - - it(`is set to false when it's passed as false`, () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - listed: false, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isListed()).to.be(false); - }); - }); - }); - - describe('pluginId', () => { - describe('does not match a kbnServer plugin', () => { - it('throws an error at instantiation', () => { - expect(() => { - createUiApp(createStubUiAppSpec({ pluginId: 'foo' })); - }).to.throwException((error) => { - expect(error.message).to.match(/Unknown plugin id/); - }); - }); - }); - }); - - describe('#getMainModuleId', () => { - it('returns undefined by default', () => { - const app = createUiApp({ id: 'foo' }); - expect(app.getMainModuleId()).to.be(undefined); - }); - - it('returns main module id', () => { - const app = createUiApp({ id: 'foo', main: 'bar' }); - expect(app.getMainModuleId()).to.be('bar'); - }); - }); -}); diff --git a/src/legacy/ui/ui_apps/index.js b/src/legacy/ui/ui_apps/index.js deleted file mode 100644 index d64848b2c1a92..0000000000000 --- a/src/legacy/ui/ui_apps/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { uiAppsMixin } from './ui_apps_mixin'; diff --git a/src/legacy/ui/ui_apps/ui_app.js b/src/legacy/ui/ui_apps/ui_app.js deleted file mode 100644 index 7da9e39394deb..0000000000000 --- a/src/legacy/ui/ui_apps/ui_app.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { UiNavLink } from '../ui_nav_links'; - -export class UiApp { - constructor(kbnServer, spec) { - const { - pluginId, - id = pluginId, - main, - title, - order = 0, - icon, - euiIconType, - hidden, - linkToLastSubUrl, - disableSubUrlTracking, - listed, - category, - url = `/app/${id}`, - } = spec; - - if (!id) { - throw new Error('Every app must specify an id'); - } - - this._id = id; - this._main = main; - this._title = title; - this._order = order; - this._icon = icon; - this._euiIconType = euiIconType; - this._linkToLastSubUrl = linkToLastSubUrl; - this._disableSubUrlTracking = disableSubUrlTracking; - this._category = category; - this._hidden = hidden; - this._listed = listed; - this._url = url; - this._pluginId = pluginId; - this._kbnServer = kbnServer; - - if (this._pluginId && !this._getPlugin()) { - throw new Error(`Unknown plugin id "${this._pluginId}"`); - } - - if (!this.isHidden()) { - // unless an app is hidden it gets a navlink, but we only respond to `getNavLink()` - // if the app is also listed. This means that all apps in the kibanaPayload will - // have a navLink property since that list includes all normally accessible apps - this._navLink = new UiNavLink({ - id: this._id, - title: this._title, - order: this._order, - icon: this._icon, - euiIconType: this._euiIconType, - url: this._url, - linkToLastSubUrl: this._linkToLastSubUrl, - disableSubUrlTracking: this._disableSubUrlTracking, - category: this._category, - }); - } - } - - getId() { - return this._id; - } - - getPluginId() { - const plugin = this._getPlugin(); - return plugin ? plugin.id : undefined; - } - - isHidden() { - return !!this._hidden; - } - - isListed() { - return !this.isHidden() && (this._listed == null || !!this._listed); - } - - getNavLink() { - if (this.isListed()) { - return this._navLink; - } - } - - getMainModuleId() { - return this._main; - } - - _getPlugin() { - const pluginId = this._pluginId; - const { plugins } = this._kbnServer; - - return pluginId ? plugins.find((plugin) => plugin.id === pluginId) : undefined; - } - - toJSON() { - return { - id: this._id, - title: this._title, - icon: this._icon, - euiIconType: this._euiIconType, - main: this._main, - navLink: this._navLink, - linkToLastSubUrl: this._linkToLastSubUrl, - disableSubUrlTracking: this._disableSubUrlTracking, - category: this._category, - }; - } -} diff --git a/src/legacy/ui/ui_apps/ui_apps_mixin.js b/src/legacy/ui/ui_apps/ui_apps_mixin.js deleted file mode 100644 index c80b12a46bee3..0000000000000 --- a/src/legacy/ui/ui_apps/ui_apps_mixin.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { UiApp } from './ui_app'; - -/** - * @typedef {import('../../server/kbn_server').default} KbnServer - */ - -/** - * - * @param {KbnServer} kbnServer - * @param {KbnServer['server']} server - */ -export function uiAppsMixin(kbnServer, server) { - const { uiAppSpecs = [] } = kbnServer.uiExports; - const existingIds = new Set(); - const appsById = new Map(); - const hiddenAppsById = new Map(); - - kbnServer.uiApps = uiAppSpecs.map((spec) => { - const app = new UiApp(kbnServer, spec); - const id = app.getId(); - - if (!existingIds.has(id)) { - existingIds.add(id); - } else { - throw new Error(`Unable to create two apps with the id ${id}.`); - } - - if (app.isHidden()) { - hiddenAppsById.set(id, app); - } else { - appsById.set(id, app); - } - - return app; - }); - - server.decorate('server', 'getAllUiApps', () => kbnServer.uiApps.slice(0)); - server.decorate('server', 'getUiAppById', (id) => appsById.get(id)); - server.decorate('server', 'getHiddenUiAppById', (id) => hiddenAppsById.get(id)); - server.decorate('server', 'injectUiAppVars', (appId, provider) => - kbnServer.newPlatform.__internals.legacy.injectUiAppVars(appId, provider) - ); - server.decorate( - 'server', - 'getInjectedUiAppVars', - async (appId) => await kbnServer.newPlatform.__internals.legacy.getInjectedUiAppVars(appId) - ); -} diff --git a/src/legacy/ui/ui_apps/ui_apps_mixin.test.js b/src/legacy/ui/ui_apps/ui_apps_mixin.test.js deleted file mode 100644 index 048358edfc10b..0000000000000 --- a/src/legacy/ui/ui_apps/ui_apps_mixin.test.js +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { LegacyInternals } from '../../../core/server'; - -import { uiAppsMixin } from './ui_apps_mixin'; - -jest.mock('./ui_app', () => ({ - UiApp: class StubUiApp { - constructor(kbnServer, spec) { - this._id = spec.id; - this._hidden = !!spec.hidden; - } - getId() { - return this._id; - } - isHidden() { - return this._hidden; - } - }, -})); - -describe('UiAppsMixin', () => { - let kbnServer; - let server; - let uiExports; - - beforeEach(() => { - uiExports = { - uiAppSpecs: [ - { - id: 'foo', - hidden: true, - }, - { - id: 'bar', - hidden: false, - }, - ], - }; - server = { - decorate: jest.fn((type, name, value) => { - if (type !== 'server') { - return; - } - - server[name] = value; - }), - }; - kbnServer = { - uiExports, - newPlatform: { - __internals: { - legacy: new LegacyInternals(uiExports, {}, server), - }, - }, - }; - - uiAppsMixin(kbnServer, server); - }); - - it('creates kbnServer.uiApps from uiExports', () => { - expect(kbnServer.uiApps).toMatchSnapshot(); - }); - - it('decorates server with methods', () => { - expect(server.decorate.mock.calls).toMatchSnapshot(); - }); - - describe('server.getAllUiApps()', () => { - it('returns hidden and non-hidden apps', () => { - expect(server.getAllUiApps()).toMatchSnapshot(); - }); - }); - - describe('server.getUiAppById()', () => { - it('returns non-hidden apps when requested, undefined for non-hidden and unknown apps', () => { - expect(server.getUiAppById('foo')).toBe(undefined); - expect(server.getUiAppById('bar')).toMatchSnapshot(); - expect(server.getUiAppById('baz')).toBe(undefined); - }); - }); - - describe('server.getHiddenUiAppById()', () => { - it('returns hidden apps when requested, undefined for non-hidden and unknown apps', () => { - expect(server.getHiddenUiAppById('foo')).toMatchSnapshot(); - expect(server.getHiddenUiAppById('bar')).toBe(undefined); - expect(server.getHiddenUiAppById('baz')).toBe(undefined); - }); - }); - - describe('server.injectUiAppVars()/server.getInjectedUiAppVars()', () => { - it('stored injectVars provider and returns provider result when requested', async () => { - server.injectUiAppVars('foo', () => ({ - thisIsFoo: true, - })); - - server.injectUiAppVars('bar', async () => ({ - thisIsFoo: false, - })); - - await expect(server.getInjectedUiAppVars('foo')).resolves.toMatchSnapshot('foo'); - await expect(server.getInjectedUiAppVars('bar')).resolves.toMatchSnapshot('bar'); - await expect(server.getInjectedUiAppVars('baz')).resolves.toEqual({}); - }); - - it('merges injected vars provided by multiple providers in the order they are registered', async () => { - server.injectUiAppVars('foo', () => ({ - foo: true, - bar: true, - baz: true, - })); - - server.injectUiAppVars('foo', async () => ({ - bar: false, - box: true, - })); - - server.injectUiAppVars('foo', async () => ({ - baz: 1, - })); - - await expect(server.getInjectedUiAppVars('foo')).resolves.toMatchSnapshot('foo'); - await expect(server.getInjectedUiAppVars('bar')).resolves.toEqual({}); - await expect(server.getInjectedUiAppVars('baz')).resolves.toEqual({}); - }); - }); -}); diff --git a/src/legacy/ui/ui_exports/README.md b/src/legacy/ui/ui_exports/README.md index ab81febe67993..7fb117b1c25b9 100644 --- a/src/legacy/ui/ui_exports/README.md +++ b/src/legacy/ui/ui_exports/README.md @@ -88,7 +88,6 @@ This reducer format was chosen so that it will be easier to look back at these r The [`ui_exports/ui_export_defaults`][UiExportDefaults] module defines the default shape of the uiExports object produced by `collectUiExports()`. The defaults generally describe the `uiExports` from the UI System itself, like default visTypes and such. -[UiApp]: ../ui_apps/ui_app.js "UiApp class definition" [UiExportDefaults]: ./ui_export_defaults.js "uiExport defaults definition" [UiExportTypes]: ./ui_export_types/index.js "Index of default ui_export_types module" [UiAppExportType]: ./ui_export_types/ui_apps.js "UiApp extension type definition" diff --git a/src/legacy/ui/ui_mixin.js b/src/legacy/ui/ui_mixin.js index 54da001d20669..caa0ca4890661 100644 --- a/src/legacy/ui/ui_mixin.js +++ b/src/legacy/ui/ui_mixin.js @@ -17,10 +17,8 @@ * under the License. */ -import { uiAppsMixin } from './ui_apps'; import { uiRenderMixin } from './ui_render'; export async function uiMixin(kbnServer) { - await kbnServer.mixin(uiAppsMixin); await kbnServer.mixin(uiRenderMixin); } diff --git a/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js b/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js deleted file mode 100644 index 42368722f11ff..0000000000000 --- a/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; - -import { UiNavLink } from '../ui_nav_link'; - -describe('UiNavLink', () => { - describe('constructor', () => { - it('initializes the object properties as expected', () => { - const spec = { - id: 'discover', - title: 'Discover', - order: -1003, - url: '/app/discover#/', - euiIconType: 'discoverApp', - hidden: true, - disabled: true, - }; - - const link = new UiNavLink(spec); - expect(link.toJSON()).to.eql({ - id: spec.id, - title: spec.title, - order: spec.order, - url: spec.url, - subUrlBase: spec.url, - icon: spec.icon, - euiIconType: spec.euiIconType, - hidden: spec.hidden, - disabled: spec.disabled, - category: undefined, - - // defaults - linkToLastSubUrl: true, - disableSubUrlTracking: undefined, - tooltip: '', - }); - }); - - it('initializes the order property to 0 when order is not specified in the spec', () => { - const spec = { - id: 'discover', - title: 'Discover', - url: '/app/discover#/', - }; - const link = new UiNavLink(spec); - - expect(link.toJSON()).to.have.property('order', 0); - }); - - it('initializes the linkToLastSubUrl property to false when false is specified in the spec', () => { - const spec = { - id: 'discover', - title: 'Discover', - order: -1003, - url: '/app/discover#/', - linkToLastSubUrl: false, - }; - const link = new UiNavLink(spec); - - expect(link.toJSON()).to.have.property('linkToLastSubUrl', false); - }); - - it('initializes the linkToLastSubUrl property to true by default', () => { - const spec = { - id: 'discover', - title: 'Discover', - order: -1003, - url: '/app/discover#/', - }; - const link = new UiNavLink(spec); - - expect(link.toJSON()).to.have.property('linkToLastSubUrl', true); - }); - - it('initializes the hidden property to false by default', () => { - const spec = { - id: 'discover', - title: 'Discover', - order: -1003, - url: '/app/discover#/', - }; - const link = new UiNavLink(spec); - - expect(link.toJSON()).to.have.property('hidden', false); - }); - - it('initializes the disabled property to false by default', () => { - const spec = { - id: 'discover', - title: 'Discover', - order: -1003, - url: '/app/discover#/', - }; - const link = new UiNavLink(spec); - - expect(link.toJSON()).to.have.property('disabled', false); - }); - - it('initializes the tooltip property to an empty string by default', () => { - const spec = { - id: 'discover', - title: 'Discover', - order: -1003, - url: '/app/discover#/', - }; - const link = new UiNavLink(spec); - - expect(link.toJSON()).to.have.property('tooltip', ''); - }); - }); -}); diff --git a/src/legacy/ui/ui_nav_links/ui_nav_link.js b/src/legacy/ui/ui_nav_links/ui_nav_link.js deleted file mode 100644 index f56809f7ebb80..0000000000000 --- a/src/legacy/ui/ui_nav_links/ui_nav_link.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export class UiNavLink { - constructor(spec) { - const { - id, - title, - order = 0, - url, - subUrlBase, - disableSubUrlTracking, - icon, - euiIconType, - linkToLastSubUrl = true, - hidden = false, - disabled = false, - tooltip = '', - category, - } = spec; - - this._id = id; - this._title = title; - this._order = order; - this._url = url; - this._subUrlBase = subUrlBase || url; - this._disableSubUrlTracking = disableSubUrlTracking; - this._icon = icon; - this._euiIconType = euiIconType; - this._linkToLastSubUrl = linkToLastSubUrl; - this._hidden = hidden; - this._disabled = disabled; - this._tooltip = tooltip; - this._category = category; - } - - getOrder() { - return this._order; - } - - toJSON() { - return { - id: this._id, - title: this._title, - order: this._order, - url: this._url, - subUrlBase: this._subUrlBase, - icon: this._icon, - euiIconType: this._euiIconType, - linkToLastSubUrl: this._linkToLastSubUrl, - disableSubUrlTracking: this._disableSubUrlTracking, - hidden: this._hidden, - disabled: this._disabled, - tooltip: this._tooltip, - category: this._category, - }; - } -} diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 8cc2cd1321a62..cd8dcf5aff71d 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -182,22 +182,16 @@ export function uiRenderMixin(kbnServer, server, config) { path: '/app/{id}/{any*}', method: 'GET', async handler(req, h) { - const id = req.params.id; - const app = server.getUiAppById(id); try { - return await h.renderApp(app); + return await h.renderApp(); } catch (err) { throw Boom.boomify(err); } }, }); - async function renderApp( - h, - app = { getId: () => 'core' }, - includeUserSettings = true, - overrides = {} - ) { + async function renderApp(h) { + const app = { getId: () => 'core' }; const { http } = kbnServer.newPlatform.setup.core; const { rendering, @@ -209,22 +203,17 @@ export function uiRenderMixin(kbnServer, server, config) { ); const vars = await legacy.getVars(app.getId(), h.request, { apmConfig: getApmConfig(h.request.path), - ...overrides, }); const content = await rendering.render(h.request, uiSettings, { app, - includeUserSettings, + includeUserSettings: true, vars, }); return h.response(content).type('text/html').header('content-security-policy', http.csp.header); } - server.decorate('toolkit', 'renderApp', function (app, overrides) { - return renderApp(this, app, true, overrides); - }); - - server.decorate('toolkit', 'renderAppWithDefaultConfig', function (app) { - return renderApp(this, app, false); + server.decorate('toolkit', 'renderApp', function () { + return renderApp(this); }); } diff --git a/src/legacy/utils/artifact_type.ts b/src/legacy/utils/artifact_type.ts index 69f728e9e2220..ef471ef8e050d 100644 --- a/src/legacy/utils/artifact_type.ts +++ b/src/legacy/utils/artifact_type.ts @@ -17,7 +17,6 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { pkg } from '../../core/server/utils'; export const IS_KIBANA_DISTRIBUTABLE = pkg.build && pkg.build.distributable === true; export const IS_KIBANA_RELEASE = pkg.build && pkg.build.release === true; diff --git a/src/legacy/utils/index.d.ts b/src/legacy/utils/index.d.ts index c294c79542bbe..a57caad1d34bf 100644 --- a/src/legacy/utils/index.d.ts +++ b/src/legacy/utils/index.d.ts @@ -18,16 +18,3 @@ */ export function unset(object: object, rawPath: string): void; - -export { - concatStreamProviders, - createConcatStream, - createFilterStream, - createIntersperseStream, - createListStream, - createMapStream, - createPromiseFromStreams, - createReduceStream, - createReplaceStream, - createSplitStream, -} from './streams'; diff --git a/src/legacy/utils/index.js b/src/legacy/utils/index.js index 4274fb2e4901a..529b1ddfd8a4d 100644 --- a/src/legacy/utils/index.js +++ b/src/legacy/utils/index.js @@ -23,15 +23,3 @@ export { deepCloneWithBuffers } from './deep_clone_with_buffers'; export { unset } from './unset'; export { IS_KIBANA_DISTRIBUTABLE } from './artifact_type'; export { IS_KIBANA_RELEASE } from './artifact_type'; - -export { - concatStreamProviders, - createConcatStream, - createIntersperseStream, - createListStream, - createPromiseFromStreams, - createReduceStream, - createSplitStream, - createMapStream, - createReplaceStream, -} from './streams'; diff --git a/src/legacy/utils/streams/index.d.ts b/src/legacy/utils/streams/index.d.ts deleted file mode 100644 index 470b5d9fa3505..0000000000000 --- a/src/legacy/utils/streams/index.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Readable, Writable, Transform, TransformOptions } from 'stream'; - -export function concatStreamProviders( - sourceProviders: Array<() => Readable>, - options: TransformOptions -): Transform; -export function createIntersperseStream(intersperseChunk: string | Buffer): Transform; -export function createSplitStream(splitChunk: T): Transform; -export function createListStream(items: any | any[]): Readable; -export function createReduceStream(reducer: (value: any, chunk: T, enc: string) => T): Transform; -export function createPromiseFromStreams([first, ...rest]: [Readable, ...Writable[]]): Promise< - T ->; -export function createConcatStream(initial?: any): Transform; -export function createMapStream(fn: (value: T, i: number) => void): Transform; -export function createReplaceStream(toReplace: string, replacement: string | Buffer): Transform; -export function createFilterStream(fn: (obj: T) => boolean): Transform; diff --git a/src/plugins/console/server/lib/spec_definitions/js/mappings.ts b/src/plugins/console/server/lib/spec_definitions/js/mappings.ts index d09637b05a3cb..aa09278d07553 100644 --- a/src/plugins/console/server/lib/spec_definitions/js/mappings.ts +++ b/src/plugins/console/server/lib/spec_definitions/js/mappings.ts @@ -17,8 +17,6 @@ * under the License. */ -import _ from 'lodash'; - import { SpecDefinitionsService } from '../../../services'; import { BOOLEAN } from './shared'; @@ -159,28 +157,25 @@ export const mappings = (specService: SpecDefinitionsService) => { // dates format: { - __one_of: _.flatten([ - _.map( - [ - 'date', - 'date_time', - 'date_time_no_millis', - 'ordinal_date', - 'ordinal_date_time', - 'ordinal_date_time_no_millis', - 'time', - 'time_no_millis', - 't_time', - 't_time_no_millis', - 'week_date', - 'week_date_time', - 'week_date_time_no_millis', - ], - function (s) { - return ['basic_' + s, 'strict_' + s]; - } - ), - [ + __one_of: [ + ...[ + 'date', + 'date_time', + 'date_time_no_millis', + 'ordinal_date', + 'ordinal_date_time', + 'ordinal_date_time_no_millis', + 'time', + 'time_no_millis', + 't_time', + 't_time_no_millis', + 'week_date', + 'week_date_time', + 'week_date_time_no_millis', + ].map(function (s) { + return ['basic_' + s, 'strict_' + s]; + }), + ...[ 'date', 'date_hour', 'date_hour_minute', @@ -214,7 +209,7 @@ export const mappings = (specService: SpecDefinitionsService) => { 'epoch_millis', 'epoch_second', ], - ]), + ], }, fielddata: { diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 09357072a13a6..3bd4d66a693b1 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -35,5 +35,5 @@ interface Services { */ export function createSavedDashboardLoader(services: Services) { const SavedDashboard = createSavedDashboardClass(services); - return new SavedObjectLoader(SavedDashboard, services.savedObjectsClient, services.chrome); + return new SavedObjectLoader(SavedDashboard, services.savedObjectsClient); } diff --git a/src/plugins/data/common/es_query/filters/get_display_value.ts b/src/plugins/data/common/es_query/filters/get_display_value.ts index 10b4dab3f46ef..28ba0ab629e8f 100644 --- a/src/plugins/data/common/es_query/filters/get_display_value.ts +++ b/src/plugins/data/common/es_query/filters/get_display_value.ts @@ -17,30 +17,26 @@ * under the License. */ -import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { IIndexPattern, IFieldType } from '../..'; +import { IIndexPattern } from '../..'; import { getIndexPatternFromFilter } from './get_index_pattern_from_filter'; import { Filter } from '../filters'; function getValueFormatter(indexPattern?: IIndexPattern, key?: string) { - if (!indexPattern || !key) return; + // checking getFormatterForField exists because there is at least once case where an index pattern + // is an object rather than an IndexPattern class + if (!indexPattern || !indexPattern.getFormatterForField || !key) return; - let format = get(indexPattern, ['fields', 'byName', key, 'format']); - if (!format && (indexPattern.fields as any).getByName) { - // TODO: Why is indexPatterns sometimes a map and sometimes an array? - const field: IFieldType = (indexPattern.fields as any).getByName(key); - if (!field) { - throw new Error( - i18n.translate('data.filter.filterBar.fieldNotFound', { - defaultMessage: 'Field {key} not found in index pattern {indexPattern}', - values: { key, indexPattern: indexPattern.title }, - }) - ); - } - format = field.format; + const field = indexPattern.fields.find((f) => f.name === key); + if (!field) { + throw new Error( + i18n.translate('data.filter.filterBar.fieldNotFound', { + defaultMessage: 'Field {key} not found in index pattern {indexPattern}', + values: { key, indexPattern: indexPattern.title }, + }) + ); } - return format; + return indexPattern.getFormatterForField(field); } export function getDisplayValueFromFilter(filter: Filter, indexPatterns: IIndexPattern[]): string { diff --git a/src/plugins/data/common/field_formats/converters/source.ts b/src/plugins/data/common/field_formats/converters/source.ts index 9c81bb011e127..e7dce82c725d2 100644 --- a/src/plugins/data/common/field_formats/converters/source.ts +++ b/src/plugins/data/common/field_formats/converters/source.ts @@ -60,7 +60,7 @@ export class SourceFormat extends FieldFormat { textConvert: TextContextTypeConvert = (value) => JSON.stringify(value); htmlConvert: HtmlContextTypeConvert = (value, options = {}) => { - const { field, hit } = options; + const { field, hit, indexPattern } = options; if (!field) { const converter = this.getConverterFor('text') as Function; @@ -69,7 +69,7 @@ export class SourceFormat extends FieldFormat { } const highlights = (hit && hit.highlight) || {}; - const formatted = field.indexPattern.formatHit(hit); + const formatted = indexPattern.formatHit(hit); const highlightPairs: any[] = []; const sourcePairs: any[] = []; const isShortDots = this.getConfig!(UI_SETTINGS.SHORT_DOTS_ENABLE); diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index 4b46adf399363..dbc3693c99779 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -181,11 +181,11 @@ export class FieldFormatsRegistry { * @param {ES_FIELD_TYPES[]} esTypes * @return {FieldFormat} */ - getDefaultInstancePlain( + getDefaultInstancePlain = ( fieldType: KBN_FIELD_TYPES, esTypes?: ES_FIELD_TYPES[], params: Record = {} - ): FieldFormat { + ): FieldFormat => { const conf = this.getDefaultConfig(fieldType, esTypes); const instanceParams = { ...conf.params, @@ -193,7 +193,7 @@ export class FieldFormatsRegistry { }; return this.getInstance(conf.id, instanceParams); - } + }; /** * Returns a cache key built by the given variables for caching in memoized * Where esType contains fieldType, fieldType is returned diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index daa44b2b0f85b..af956a20c0dc5 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -27,6 +27,7 @@ export type FieldFormatsContentType = 'html' | 'text'; /** @internal **/ export interface HtmlContextTypeOptions { field?: any; + indexPattern?: any; hit?: Record; } diff --git a/src/test_utils/public/ng_mock.js b/src/plugins/data/common/index_patterns/errors.ts similarity index 74% rename from src/test_utils/public/ng_mock.js rename to src/plugins/data/common/index_patterns/errors.ts index 01bab4ce0a872..3d92bae1968fb 100644 --- a/src/test_utils/public/ng_mock.js +++ b/src/plugins/data/common/index_patterns/errors.ts @@ -17,15 +17,13 @@ * under the License. */ -import angular from 'angular'; -import 'angular-mocks'; -import 'mocha'; +import { FieldSpec } from './types'; -if (angular.mocks) { - throw new Error( - "Don't require angular-mocks directly or the tests " + - "can't setup correctly, use the ngMock module instead." - ); +export class FieldTypeUnknownError extends Error { + public readonly fieldSpec: FieldSpec; + constructor(message: string, spec: FieldSpec) { + super(message); + this.name = 'FieldTypeUnknownError'; + this.fieldSpec = spec; + } } - -export default angular.mock; diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap index e61593f6bfb27..4279dd320ad62 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap @@ -14,7 +14,7 @@ Object { }, "count": 1, "esTypes": Array [ - "type", + "text", ], "lang": "lang", "name": "name", @@ -30,7 +30,7 @@ Object { "path": "path", }, }, - "type": "type", + "type": "string", } `; @@ -48,7 +48,7 @@ Object { }, "count": 1, "esTypes": Array [ - "type", + "text", ], "format": Object { "id": "number", @@ -70,6 +70,6 @@ Object { "path": "path", }, }, - "type": "type", + "type": "string", } `; diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts index d2489a5d1f7e3..4cf6075869851 100644 --- a/src/plugins/data/common/index_patterns/fields/field_list.ts +++ b/src/plugins/data/common/index_patterns/fields/field_list.ts @@ -35,6 +35,7 @@ export interface IIndexPatternFieldList extends Array { removeAll(): void; replaceAll(specs: FieldSpec[]): void; update(field: FieldSpec): void; + toSpec(options?: { getFormatterForField?: IndexPattern['getFormatterForField'] }): FieldSpec[]; } export type CreateIndexPatternFieldList = ( @@ -44,87 +45,79 @@ export type CreateIndexPatternFieldList = ( onNotification?: OnNotification ) => IIndexPatternFieldList; -export class FieldList extends Array implements IIndexPatternFieldList { - private byName: FieldMap = new Map(); - private groups: Map = new Map(); - private indexPattern: IndexPattern; - private shortDotsEnable: boolean; - private onNotification: OnNotification; - private setByName = (field: IndexPatternField) => this.byName.set(field.name, field); - private setByGroup = (field: IndexPatternField) => { - if (typeof this.groups.get(field.type) === 'undefined') { - this.groups.set(field.type, new Map()); +// extending the array class and using a constructor doesn't work well +// when calling filter and similar so wrapping in a callback. +// to be removed in the future +export const fieldList = ( + specs: FieldSpec[] = [], + shortDotsEnable = false +): IIndexPatternFieldList => { + class FldList extends Array implements IIndexPatternFieldList { + private byName: FieldMap = new Map(); + private groups: Map = new Map(); + private setByName = (field: IndexPatternField) => this.byName.set(field.name, field); + private setByGroup = (field: IndexPatternField) => { + if (typeof this.groups.get(field.type) === 'undefined') { + this.groups.set(field.type, new Map()); + } + this.groups.get(field.type)!.set(field.name, field); + }; + private removeByGroup = (field: IFieldType) => this.groups.get(field.type)!.delete(field.name); + private calcDisplayName = (name: string) => + shortDotsEnable ? shortenDottedString(name) : name; + constructor() { + super(); + specs.map((field) => this.add(field)); } - this.groups.get(field.type)!.set(field.name, field); - }; - private removeByGroup = (field: IFieldType) => this.groups.get(field.type)!.delete(field.name); - private calcDisplayName = (name: string) => - this.shortDotsEnable ? shortenDottedString(name) : name; - constructor( - indexPattern: IndexPattern, - specs: FieldSpec[] = [], - shortDotsEnable = false, - onNotification: OnNotification = () => {} - ) { - super(); - this.indexPattern = indexPattern; - this.shortDotsEnable = shortDotsEnable; - this.onNotification = onNotification; - specs.map((field) => this.add(field)); - } + public readonly getAll = () => [...this.byName.values()]; + public readonly getByName = (name: IndexPatternField['name']) => this.byName.get(name); + public readonly getByType = (type: IndexPatternField['type']) => [ + ...(this.groups.get(type) || new Map()).values(), + ]; + public readonly add = (field: FieldSpec) => { + const newField = new IndexPatternField(field, this.calcDisplayName(field.name)); + this.push(newField); + this.setByName(newField); + this.setByGroup(newField); + }; - public readonly getAll = () => [...this.byName.values()]; - public readonly getByName = (name: IndexPatternField['name']) => this.byName.get(name); - public readonly getByType = (type: IndexPatternField['type']) => [ - ...(this.groups.get(type) || new Map()).values(), - ]; - public readonly add = (field: FieldSpec) => { - const newField = new IndexPatternField( - this.indexPattern, - field, - this.calcDisplayName(field.name), - this.onNotification - ); - this.push(newField); - this.setByName(newField); - this.setByGroup(newField); - }; + public readonly remove = (field: IFieldType) => { + this.removeByGroup(field); + this.byName.delete(field.name); - public readonly remove = (field: IFieldType) => { - this.removeByGroup(field); - this.byName.delete(field.name); + const fieldIndex = findIndex(this, { name: field.name }); + this.splice(fieldIndex, 1); + }; - const fieldIndex = findIndex(this, { name: field.name }); - this.splice(fieldIndex, 1); - }; + public readonly update = (field: FieldSpec) => { + const newField = new IndexPatternField(field, this.calcDisplayName(field.name)); + const index = this.findIndex((f) => f.name === newField.name); + this.splice(index, 1, newField); + this.setByName(newField); + this.removeByGroup(newField); + this.setByGroup(newField); + }; - public readonly update = (field: FieldSpec) => { - const newField = new IndexPatternField( - this.indexPattern, - field, - this.calcDisplayName(field.name), - this.onNotification - ); - const index = this.findIndex((f) => f.name === newField.name); - this.splice(index, 1, newField); - this.setByName(newField); - this.removeByGroup(newField); - this.setByGroup(newField); - }; + public readonly removeAll = () => { + this.length = 0; + this.byName.clear(); + this.groups.clear(); + }; - public readonly removeAll = () => { - this.length = 0; - this.byName.clear(); - this.groups.clear(); - }; + public readonly replaceAll = (spcs: FieldSpec[]) => { + this.removeAll(); + spcs.forEach(this.add); + }; - public readonly replaceAll = (specs: FieldSpec[]) => { - this.removeAll(); - specs.forEach(this.add); - }; + public toSpec({ + getFormatterForField, + }: { + getFormatterForField?: IndexPattern['getFormatterForField']; + } = {}) { + return [...this.map((field) => field.toSpec({ getFormatterForField }))]; + } + } - public readonly toSpec = () => { - return [...this.map((field) => field.toSpec())]; - }; -} + return new FldList(); +}; diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts index 0cd0fe8324809..3c4fac81c2c7c 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts @@ -19,7 +19,7 @@ import { IndexPatternField } from './index_pattern_field'; import { IndexPattern } from '../index_patterns'; -import { KBN_FIELD_TYPES } from '../../../common'; +import { KBN_FIELD_TYPES, FieldFormat } from '../../../common'; import { FieldSpec } from '../types'; describe('Field', function () { @@ -28,21 +28,16 @@ describe('Field', function () { } function getField(values = {}) { - return new IndexPatternField( - fieldValues.indexPattern as IndexPattern, - { ...fieldValues, ...values }, - 'displayName', - () => {} - ); + return new IndexPatternField({ ...fieldValues, ...values }, 'displayName'); } const fieldValues = { name: 'name', - type: 'type', + type: 'string', script: 'script', lang: 'lang', count: 1, - esTypes: ['type'], + esTypes: ['text'], aggregatable: true, filterable: true, searchable: true, @@ -125,7 +120,7 @@ describe('Field', function () { const fieldB = getField({ indexed: true, type: KBN_FIELD_TYPES.STRING }); expect(fieldB.sortable).toEqual(true); - const fieldC = getField({ indexed: false }); + const fieldC = getField({ indexed: false, aggregatable: false, scripted: false }); expect(fieldC.sortable).toEqual(false); }); @@ -139,31 +134,26 @@ describe('Field', function () { const fieldC = getField({ indexed: true, type: KBN_FIELD_TYPES.STRING }); expect(fieldC.filterable).toEqual(true); - const fieldD = getField({ scripted: false, indexed: false }); + const fieldD = getField({ scripted: false, indexed: false, searchable: false }); expect(fieldD.filterable).toEqual(false); }); it('exports the property to JSON', () => { - const field = new IndexPatternField( - { fieldFormatMap: { name: {} } } as IndexPattern, - fieldValues, - 'displayName', - () => {} - ); + const field = new IndexPatternField(fieldValues, 'displayName'); expect(flatten(field)).toMatchSnapshot(); }); it('spec snapshot', () => { - const field = new IndexPatternField( - { - fieldFormatMap: { - name: { toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }) }, - }, - } as IndexPattern, - fieldValues, - 'displayName', - () => {} - ); - expect(field.toSpec()).toMatchSnapshot(); + const field = new IndexPatternField(fieldValues, 'displayName'); + const getFormatterForField = () => + ({ + toJSON: () => ({ + id: 'number', + params: { + pattern: '$0,0.[00]', + }, + }), + } as FieldFormat); + expect(field.toSpec({ getFormatterForField })).toMatchSnapshot(); }); }); diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 965f1a7f63065..7f72bfe55c7cd 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -20,40 +20,27 @@ import { i18n } from '@kbn/i18n'; import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; -import { FieldFormat } from '../../field_formats'; import { IFieldType } from './types'; -import { OnNotification, FieldSpec } from '../types'; - -import { IndexPattern } from '../index_patterns'; +import { FieldSpec, IndexPattern } from '../..'; +import { FieldTypeUnknownError } from '../errors'; export class IndexPatternField implements IFieldType { readonly spec: FieldSpec; // not writable or serialized - readonly indexPattern: IndexPattern; readonly displayName: string; private readonly kbnFieldType: KbnFieldType; - constructor( - indexPattern: IndexPattern, - spec: FieldSpec, - displayName: string, - onNotification: OnNotification - ) { - this.indexPattern = indexPattern; + constructor(spec: FieldSpec, displayName: string) { this.spec = { ...spec, type: spec.name === '_source' ? '_source' : spec.type }; this.displayName = displayName; this.kbnFieldType = getKbnFieldType(spec.type); if (spec.type && this.kbnFieldType?.name === KBN_FIELD_TYPES.UNKNOWN) { - const title = i18n.translate('data.indexPatterns.unknownFieldHeader', { - values: { type: spec.type }, - defaultMessage: 'Unknown field type {type}', - }); - const text = i18n.translate('data.indexPatterns.unknownFieldErrorMessage', { - values: { name: spec.name, title: indexPattern.title }, - defaultMessage: 'Field {name} in indexPattern {title} is using an unknown field type.', + const msg = i18n.translate('data.indexPatterns.unknownFieldTypeErrorMsg', { + values: { type: spec.type, name: spec.name }, + defaultMessage: `Field '{name}' Unknown field type '{type}'`, }); - onNotification({ title, text, color: 'danger', iconType: 'alert' }); + throw new FieldTypeUnknownError(msg, spec); } } @@ -143,10 +130,6 @@ export class IndexPatternField implements IFieldType { return this.aggregatable; } - public get format(): FieldFormat { - return this.indexPattern.getFormatterForField(this); - } - public toJSON() { return { count: this.count, @@ -165,7 +148,11 @@ export class IndexPatternField implements IFieldType { }; } - public toSpec() { + public toSpec({ + getFormatterForField, + }: { + getFormatterForField?: IndexPattern['getFormatterForField']; + } = {}) { return { count: this.count, script: this.script, @@ -179,7 +166,7 @@ export class IndexPatternField implements IFieldType { aggregatable: this.aggregatable, readFromDocValues: this.readFromDocValues, subType: this.subType, - format: this.indexPattern?.fieldFormatMap[this.name]?.toJSON() || undefined, + format: getFormatterForField ? getFormatterForField(this).toJSON() : undefined, }; } } diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index 558b5b57dce40..5814760601a67 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { FieldSpec, IFieldSubType } from '../types'; +import { FieldSpec, IFieldSubType, IndexPattern } from '../..'; export interface IFieldType { name: string; @@ -38,5 +38,5 @@ export interface IFieldType { subType?: IFieldSubType; displayName?: string; format?: any; - toSpec?: () => FieldSpec; + toSpec?: (options?: { getFormatterForField?: IndexPattern['getFormatterForField'] }) => FieldSpec; } diff --git a/src/plugins/data/common/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index.ts index 51a642b775c29..08f478404be2c 100644 --- a/src/plugins/data/common/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index.ts @@ -20,3 +20,5 @@ export * from './fields'; export * from './types'; export { IndexPatternsService } from './index_patterns'; +export type { IndexPattern } from './index_patterns'; +export * from './errors'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index 047ac836a87d1..a0c380ec55bf6 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -32,7 +32,12 @@ Object { "esTypes": Array [ "boolean", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "ssl", "readFromDocValues": true, @@ -49,7 +54,12 @@ Object { "esTypes": Array [ "date", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "@timestamp", "readFromDocValues": true, @@ -66,7 +76,12 @@ Object { "esTypes": Array [ "date", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "time", "readFromDocValues": true, @@ -83,7 +98,12 @@ Object { "esTypes": Array [ "keyword", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "@tags", "readFromDocValues": true, @@ -100,7 +120,12 @@ Object { "esTypes": Array [ "date", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "utc_time", "readFromDocValues": true, @@ -117,7 +142,12 @@ Object { "esTypes": Array [ "integer", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "phpmemory", "readFromDocValues": true, @@ -134,7 +164,12 @@ Object { "esTypes": Array [ "ip", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "ip", "readFromDocValues": true, @@ -151,7 +186,12 @@ Object { "esTypes": Array [ "attachment", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "request_body", "readFromDocValues": true, @@ -168,7 +208,12 @@ Object { "esTypes": Array [ "geo_point", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "point", "readFromDocValues": true, @@ -185,7 +230,12 @@ Object { "esTypes": Array [ "geo_shape", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "area", "readFromDocValues": false, @@ -202,7 +252,12 @@ Object { "esTypes": Array [ "murmur3", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "hashed", "readFromDocValues": false, @@ -219,7 +274,12 @@ Object { "esTypes": Array [ "geo_point", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "geo.coordinates", "readFromDocValues": true, @@ -236,7 +296,12 @@ Object { "esTypes": Array [ "text", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "extension", "readFromDocValues": false, @@ -253,7 +318,12 @@ Object { "esTypes": Array [ "keyword", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "extension.keyword", "readFromDocValues": true, @@ -274,7 +344,12 @@ Object { "esTypes": Array [ "text", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "machine.os", "readFromDocValues": false, @@ -291,7 +366,12 @@ Object { "esTypes": Array [ "keyword", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "machine.os.raw", "readFromDocValues": true, @@ -312,7 +392,12 @@ Object { "esTypes": Array [ "keyword", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "geo.src", "readFromDocValues": true, @@ -329,7 +414,12 @@ Object { "esTypes": Array [ "_id", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "_id", "readFromDocValues": false, @@ -346,7 +436,12 @@ Object { "esTypes": Array [ "_type", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "_type", "readFromDocValues": false, @@ -363,7 +458,12 @@ Object { "esTypes": Array [ "_source", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "_source", "readFromDocValues": false, @@ -380,7 +480,12 @@ Object { "esTypes": Array [ "text", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "non-filterable", "readFromDocValues": false, @@ -397,7 +502,12 @@ Object { "esTypes": Array [ "text", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "non-sortable", "readFromDocValues": false, @@ -414,7 +524,12 @@ Object { "esTypes": Array [ "conflict", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": undefined, "name": "custom_user_field", "readFromDocValues": true, @@ -431,7 +546,12 @@ Object { "esTypes": Array [ "text", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": "expression", "name": "script string", "readFromDocValues": false, @@ -448,7 +568,12 @@ Object { "esTypes": Array [ "long", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": "expression", "name": "script number", "readFromDocValues": false, @@ -465,7 +590,12 @@ Object { "esTypes": Array [ "date", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": "painless", "name": "script date", "readFromDocValues": false, @@ -482,7 +612,12 @@ Object { "esTypes": Array [ "murmur3", ], - "format": undefined, + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, "lang": "expression", "name": "script murmur3", "readFromDocValues": false, diff --git a/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts b/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts index a0597ed4b9026..b47fef107258a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts @@ -34,9 +34,9 @@ export function formatHitProvider(indexPattern: IndexPattern, defaultFormat: any type: FieldFormatsContentType = 'html' ) { const field = indexPattern.fields.getByName(fieldName); - const format = field ? field.format : defaultFormat; + const format = field ? indexPattern.getFormatterForField(field) : defaultFormat; - return format.convert(val, type, { field, hit }); + return format.convert(val, type, { field, hit, indexPattern }); } function formatHit(hit: Record, type: string = 'html') { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index f7e1156170f03..f037a71b508a2 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -29,6 +29,7 @@ import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_ import { IndexPatternField } from '../fields'; import { fieldFormatsMock } from '../../field_formats/mocks'; +import { FieldFormat } from '../..'; class MockFieldFormatter {} @@ -150,8 +151,6 @@ describe('IndexPattern', () => { expect(indexPattern).toHaveProperty('getNonScriptedFields'); expect(indexPattern).toHaveProperty('addScriptedField'); expect(indexPattern).toHaveProperty('removeScriptedField'); - expect(indexPattern).toHaveProperty('toString'); - expect(indexPattern).toHaveProperty('toJSON'); expect(indexPattern).toHaveProperty('save'); // properties @@ -170,7 +169,6 @@ describe('IndexPattern', () => { test('should have expected properties on fields', function () { expect(indexPattern.fields[0]).toHaveProperty('displayName'); expect(indexPattern.fields[0]).toHaveProperty('filterable'); - expect(indexPattern.fields[0]).toHaveProperty('format'); expect(indexPattern.fields[0]).toHaveProperty('sortable'); expect(indexPattern.fields[0]).toHaveProperty('scripted'); }); @@ -319,16 +317,18 @@ describe('IndexPattern', () => { describe('toSpec', () => { test('should match snapshot', () => { - indexPattern.fieldFormatMap.bytes = { + const formatter = { toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }), - }; + } as FieldFormat; + indexPattern.getFormatterForField = () => formatter; expect(indexPattern.toSpec()).toMatchSnapshot(); }); test('can restore from spec', async () => { - indexPattern.fieldFormatMap.bytes = { + const formatter = { toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }), - }; + } as FieldFormat; + indexPattern.getFormatterForField = () => formatter; const spec = indexPattern.toSpec(); const restoredPattern = await create(spec.id as string); restoredPattern.initFromSpec(spec); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index ea91a9bb14e1f..0558808573580 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -26,11 +26,12 @@ import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, + FieldTypeUnknownError, FieldFormatNotFoundError, } from '../../../common'; import { findByTitle } from '../utils'; import { IndexPatternMissingIndices } from '../lib'; -import { IndexPatternField, IIndexPatternFieldList, FieldList } from '../fields'; +import { IndexPatternField, IIndexPatternFieldList, fieldList } from '../fields'; import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; @@ -125,8 +126,7 @@ export class IndexPattern implements IIndexPattern { this.shortDotsEnable = shortDotsEnable; this.metaFields = metaFields; - - this.fields = new FieldList(this, [], this.shortDotsEnable, this.onNotification); + this.fields = fieldList([], this.shortDotsEnable); this.apiClient = apiClient; this.fieldsFetcher = createFieldsFetcher(this, apiClient, metaFields); @@ -138,6 +138,22 @@ export class IndexPattern implements IIndexPattern { this.formatField = this.formatHit.formatField; } + private unknownFieldErrorNotification( + fieldType: string, + fieldName: string, + indexPatternTitle: string + ) { + const title = i18n.translate('data.indexPatterns.unknownFieldHeader', { + values: { type: fieldType }, + defaultMessage: 'Unknown field type {type}', + }); + const text = i18n.translate('data.indexPatterns.unknownFieldErrorMessage', { + values: { name: fieldName, title: indexPatternTitle }, + defaultMessage: 'Field {name} in indexPattern {title} is using an unknown field type.', + }); + this.onNotification({ title, text, color: 'danger', iconType: 'alert' }); + } + private serializeFieldFormatMap(flat: any, format: string, field: string | undefined) { if (format && field) { flat[field] = format; @@ -181,7 +197,15 @@ export class IndexPattern implements IIndexPattern { await this.refreshFields(); } else { if (specs) { - this.fields.replaceAll(specs); + try { + this.fields.replaceAll(specs); + } catch (err) { + if (err instanceof FieldTypeUnknownError) { + this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); + } else { + throw err; + } + } } } } @@ -203,7 +227,15 @@ export class IndexPattern implements IIndexPattern { this.timeFieldName = spec.timeFieldName; this.sourceFilters = spec.sourceFilters; - this.fields.replaceAll(spec.fields || []); + try { + this.fields.replaceAll(spec.fields || []); + } catch (err) { + if (err instanceof FieldTypeUnknownError) { + this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); + } else { + throw err; + } + } this.typeMeta = spec.typeMeta; this.fieldFormatMap = _.mapValues(fieldFormatMap, (mapping) => { @@ -322,7 +354,7 @@ export class IndexPattern implements IIndexPattern { title: this.title, timeFieldName: this.timeFieldName, sourceFilters: this.sourceFilters, - fields: this.fields.toSpec(), + fields: this.fields.toSpec({ getFormatterForField: this.getFormatterForField.bind(this) }), typeMeta: this.typeMeta, }; } @@ -342,19 +374,27 @@ export class IndexPattern implements IIndexPattern { throw new DuplicateField(name); } - this.fields.add({ - name, - script, - type: fieldType, - scripted: true, - lang, - aggregatable: true, - searchable: true, - count: 0, - readFromDocValues: false, - }); + try { + this.fields.add({ + name, + script, + type: fieldType, + scripted: true, + lang, + aggregatable: true, + searchable: true, + count: 0, + readFromDocValues: false, + }); + } catch (err) { + if (err instanceof FieldTypeUnknownError) { + this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); + } else { + throw err; + } - await this.save(); + await this.save(); + } } removeScriptedField(fieldName: string) { @@ -441,7 +481,7 @@ export class IndexPattern implements IIndexPattern { fields: this.mapping.fields._serialize!(this.fields), fieldFormatMap: this.mapping.fieldFormatMap._serialize!(this.fieldFormatMap), type: this.type, - typeMeta: this.mapping.typeMeta._serialize!(this.mapping), + typeMeta: this.mapping.typeMeta._serialize!(this.typeMeta), }; } @@ -572,7 +612,15 @@ export class IndexPattern implements IIndexPattern { async _fetchFields() { const fields = await this.fieldsFetcher.fetch(this); const scripted = this.getScriptedFields().map((field) => field.spec); - this.fields.replaceAll([...fields, ...scripted]); + try { + this.fields.replaceAll([...fields, ...scripted]); + } catch (err) { + if (err instanceof FieldTypeUnknownError) { + this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); + } else { + throw err; + } + } } refreshFields() { @@ -602,12 +650,4 @@ export class IndexPattern implements IIndexPattern { }); }); } - - toJSON() { - return this.id; - } - - toString() { - return '' + this.toJSON(); - } } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 0ad9ae8f2014f..fe0d14b2d9c19 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -25,7 +25,6 @@ import { createEnsureDefaultIndexPattern, EnsureDefaultIndexPattern, } from './ensure_default_index_pattern'; -import { IndexPatternField } from '../fields'; import { OnNotification, OnError, @@ -86,15 +85,6 @@ export class IndexPatternsService { ); } - public createField( - indexPattern: IndexPattern, - spec: IndexPatternField['spec'], - displayName: string, - onNotification: OnNotification - ) { - return new IndexPatternField(indexPattern, spec, displayName, onNotification); - } - private async refreshSavedObjectsCache() { this.savedObjectsCache = await this.savedObjectsClient.find({ type: 'index-pattern', diff --git a/src/plugins/data/common/search/aggs/agg_config.test.ts b/src/plugins/data/common/search/aggs/agg_config.test.ts index a443eacee731c..f6fcc29805dc4 100644 --- a/src/plugins/data/common/search/aggs/agg_config.test.ts +++ b/src/plugins/data/common/search/aggs/agg_config.test.ts @@ -25,8 +25,7 @@ import { AggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { mockAggTypesRegistry } from './test_helpers'; import { MetricAggType } from './metrics/metric_agg_type'; -import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; -import { IIndexPatternFieldList } from '../../index_patterns/fields'; +import { IndexPattern, IndexPatternField, IIndexPatternFieldList } from '../../index_patterns'; describe('AggConfig', () => { let indexPattern: IndexPattern; @@ -67,6 +66,9 @@ describe('AggConfig', () => { getByName: (name: string) => fields.find((f) => f.name === name), filter: () => fields, } as unknown) as IndexPattern['fields'], + getFormatterForField: (field: IndexPatternField) => ({ + toJSON: () => ({}), + }), } as IndexPattern; typesRegistry = mockAggTypesRegistry(); }); diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index b5747ce7bb9bd..201e9f1ec402c 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -21,12 +21,13 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { Assign, Ensure } from '@kbn/utility-types'; -import { FetchOptions, ISearchSource } from 'src/plugins/data/public'; +import { ISearchSource } from 'src/plugins/data/public'; import { ExpressionAstFunction, ExpressionAstArgument, SerializedFieldFormat, } from 'src/plugins/expressions/common'; +import { ISearchOptions } from '../es_search'; import { IAggType } from './agg_type'; import { writeParams } from './agg_params'; @@ -213,11 +214,11 @@ export class AggConfig { /** * Hook for pre-flight logic, see AggType#onSearchRequestStart - * @param {Courier.SearchSource} searchSource - * @param {Courier.FetchOptions} options + * @param {SearchSource} searchSource + * @param {ISearchOptions} options * @return {Promise} */ - onSearchRequestStart(searchSource: ISearchSource, options?: FetchOptions) { + onSearchRequestStart(searchSource: ISearchSource, options?: ISearchOptions) { if (!this.type) { return Promise.resolve(); } diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index 203eda3a907ee..282e6f3b538a4 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -20,7 +20,7 @@ import _ from 'lodash'; import { Assign } from '@kbn/utility-types'; -import { FetchOptions, ISearchSource } from 'src/plugins/data/public'; +import { ISearchOptions, ISearchSource } from 'src/plugins/data/public'; import { AggConfig, AggConfigSerialized, IAggConfig } from './agg_config'; import { IAggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; @@ -300,7 +300,7 @@ export class AggConfigs { return _.find(reqAgg.getResponseAggs(), { id }); } - onSearchRequestStart(searchSource: ISearchSource, options?: FetchOptions) { + onSearchRequestStart(searchSource: ISearchSource, options?: ISearchOptions) { return Promise.all( // @ts-ignore this.getRequestAggs().map((agg: AggConfig) => agg.onSearchRequestStart(searchSource, options)) diff --git a/src/plugins/data/common/search/aggs/agg_type.test.ts b/src/plugins/data/common/search/aggs/agg_type.test.ts index bf1136159dfe8..16a5586858ab9 100644 --- a/src/plugins/data/common/search/aggs/agg_type.test.ts +++ b/src/plugins/data/common/search/aggs/agg_type.test.ts @@ -147,6 +147,9 @@ describe('AggType Class', () => { }, }, }, + aggConfigs: { + indexPattern: { getFormatterForField: () => ({ toJSON: () => ({ id: 'format' }) }) }, + }, } as unknown) as IAggConfig; const aggType = new AggType({ name: 'name', diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 2ee604c1bf25d..1e3839038b0f7 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -271,7 +271,9 @@ export class AggType< this.getSerializedFormat = config.getSerializedFormat || ((agg: TAggConfig) => { - return agg.params.field ? agg.params.field.format.toJSON() : {}; + return agg.params.field + ? agg.aggConfigs.indexPattern.getFormatterForField(agg.params.field).toJSON() + : {}; }); this.getValue = config.getValue || ((agg: TAggConfig, bucket: any) => {}); diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts index 1a7deafb548ae..7c09d2e64e8b7 100644 --- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts +++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.ts @@ -140,7 +140,7 @@ export const buildOtherBucketAgg = ( const bucketAggs = aggConfigs.aggs.filter((agg) => agg.type.type === AggGroupNames.Buckets); const index = bucketAggs.findIndex((agg) => agg.id === aggWithOtherBucket.id); const aggs = aggConfigs.toDsl(); - const indexPattern = aggWithOtherBucket.params.field.indexPattern; + const indexPattern = aggWithOtherBucket.aggConfigs.indexPattern; // create filters aggregation const filterAgg = aggConfigs.createAggConfig( @@ -211,7 +211,7 @@ export const buildOtherBucketAgg = ( filters.push( buildExistsFilter( aggWithOtherBucket.params.field, - aggWithOtherBucket.params.field.indexPattern + aggWithOtherBucket.aggConfigs.indexPattern ) ); } @@ -264,7 +264,7 @@ export const mergeOtherBucketAggResponse = ( const phraseFilter = buildPhrasesFilter( otherAgg.params.field, requestFilterTerms, - otherAgg.params.field.indexPattern + otherAgg.aggConfigs.indexPattern ); phraseFilter.meta.negate = true; bucket.filters = [phraseFilter]; @@ -276,7 +276,7 @@ export const mergeOtherBucketAggResponse = ( ) ) { bucket.filters.push( - buildExistsFilter(otherAgg.params.field, otherAgg.params.field.indexPattern) + buildExistsFilter(otherAgg.params.field, otherAgg.aggConfigs.indexPattern) ); } aggResultBuckets.push(bucket); diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts index b57d530ef40e8..dc1d0ec0a152f 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/histogram.test.ts @@ -40,6 +40,7 @@ describe('AggConfig Filters', () => { getByName: () => field, filter: () => [field], }, + getFormatterForField: () => new BytesFormat({}, getConfig), } as any; return new AggConfigs( diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts index 30af970f55aa9..b53ae44c05075 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts @@ -30,7 +30,6 @@ describe('AggConfig Filters', () => { const getAggConfigs = () => { const field = { name: 'bytes', - format: new BytesFormat({}, getConfig), }; const indexPattern = { @@ -40,6 +39,7 @@ describe('AggConfig Filters', () => { getByName: () => field, filter: () => [field], }, + getFormatterForField: () => new BytesFormat({}, getConfig), } as any; return new AggConfigs( diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts index 95de19b96abd4..ccd1cf6e358b4 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/terms.ts @@ -27,7 +27,7 @@ import { export const createFilterTerms = (aggConfig: IBucketAggConfig, key: string, params: any) => { const field = aggConfig.params.field; - const indexPattern = field.indexPattern; + const indexPattern = aggConfig.aggConfigs.indexPattern; if (key === '__other__') { const terms = params.terms; diff --git a/src/plugins/data/common/search/aggs/buckets/date_range.ts b/src/plugins/data/common/search/aggs/buckets/date_range.ts index eda35a77afa5f..f9a3acb990fbf 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_range.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_range.ts @@ -58,7 +58,9 @@ export const getDateRangeBucketAgg = ({ getSerializedFormat(agg) { return { id: 'date_range', - params: agg.params.field ? agg.params.field.format.toJSON() : {}, + params: agg.params.field + ? agg.aggConfigs.indexPattern.getFormatterForField(agg.params.field).toJSON() + : {}, }; }, makeLabel(aggConfig) { diff --git a/src/plugins/data/common/search/aggs/buckets/ip_range.ts b/src/plugins/data/common/search/aggs/buckets/ip_range.ts index 46e0b62d0f8d7..d0a6174b011fc 100644 --- a/src/plugins/data/common/search/aggs/buckets/ip_range.ts +++ b/src/plugins/data/common/search/aggs/buckets/ip_range.ts @@ -59,7 +59,9 @@ export const getIpRangeBucketAgg = () => getSerializedFormat(agg) { return { id: 'ip_range', - params: agg.params.field ? agg.params.field.format.toJSON() : {}, + params: agg.params.field + ? agg.aggConfigs.indexPattern.getFormatterForField(agg.params.field).toJSON() + : {}, }; }, makeLabel(aggConfig) { diff --git a/src/plugins/data/common/search/aggs/buckets/range.test.ts b/src/plugins/data/common/search/aggs/buckets/range.test.ts index b23b03db6a9ec..b8241e04ea1ee 100644 --- a/src/plugins/data/common/search/aggs/buckets/range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/range.test.ts @@ -27,12 +27,6 @@ describe('Range Agg', () => { const getAggConfigs = () => { const field = { name: 'bytes', - format: new NumberFormat( - { - pattern: '0,0.[000] b', - }, - getConfig - ), }; const indexPattern = { @@ -42,6 +36,13 @@ describe('Range Agg', () => { getByName: () => field, filter: () => [field], }, + getFormatterForField: () => + new NumberFormat( + { + pattern: '0,0.[000] b', + }, + getConfig + ), } as any; return new AggConfigs( diff --git a/src/plugins/data/common/search/aggs/buckets/range.ts b/src/plugins/data/common/search/aggs/buckets/range.ts index 91a357b635950..169b234845274 100644 --- a/src/plugins/data/common/search/aggs/buckets/range.ts +++ b/src/plugins/data/common/search/aggs/buckets/range.ts @@ -78,7 +78,9 @@ export const getRangeBucketAgg = ({ getFieldFormatsStart }: RangeBucketAggDepend return key; }, getSerializedFormat(agg) { - const format = agg.params.field ? agg.params.field.format.toJSON() : {}; + const format = agg.params.field + ? agg.aggConfigs.indexPattern.getFormatterForField(agg.params.field).toJSON() + : { id: undefined, params: undefined }; return { id: 'range', params: { diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index 5c8483cf21369..1363d38748c8b 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -82,7 +82,9 @@ export const getTermsBucketAgg = () => return agg.getFieldDisplayName() + ': ' + params.order.text; }, getSerializedFormat(agg) { - const format = agg.params.field ? agg.params.field.format.toJSON() : {}; + const format = agg.params.field + ? agg.aggConfigs.indexPattern.getFormatterForField(agg.params.field).toJSON() + : { id: undefined, params: undefined }; return { id: 'terms', params: { diff --git a/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts b/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts index c6bba56f73ec7..4815ab0ac56dc 100644 --- a/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/parent_pipeline.test.ts @@ -66,9 +66,6 @@ describe('parent pipeline aggs', function () { ) => { const field = { name: 'field', - format: { - toJSON: () => ({ id: 'bytes' }), - }, }; const indexPattern = { id: '1234', @@ -77,6 +74,9 @@ describe('parent pipeline aggs', function () { getByName: () => field, filter: () => [field], }, + getFormatterForField: () => ({ + toJSON: () => ({ id: 'bytes' }), + }), } as any; const aggConfigs = new AggConfigs( diff --git a/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts b/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts index a157d225c839c..32737f7b7237d 100644 --- a/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/sibling_pipeline.test.ts @@ -72,6 +72,9 @@ describe('sibling pipeline aggs', () => { getByName: () => field, filter: () => [field], }, + getFormatterForField: () => ({ + toJSON: () => ({ id: 'bytes' }), + }), } as any; const aggConfigs = new AggConfigs( diff --git a/src/plugins/data/common/search/aggs/param_types/base.ts b/src/plugins/data/common/search/aggs/param_types/base.ts index 3a12a9a54500f..c0316c974e26f 100644 --- a/src/plugins/data/common/search/aggs/param_types/base.ts +++ b/src/plugins/data/common/search/aggs/param_types/base.ts @@ -17,7 +17,7 @@ * under the License. */ -import { FetchOptions, ISearchSource } from 'src/plugins/data/public'; +import { ISearchOptions, ISearchSource } from 'src/plugins/data/public'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { IAggConfigs } from '../agg_configs'; import { IAggConfig } from '../agg_config'; @@ -56,7 +56,7 @@ export class BaseParamType { modifyAggConfigOnSearchRequestStart: ( aggConfig: TAggConfig, searchSource?: ISearchSource, - options?: FetchOptions + options?: ISearchOptions ) => void; constructor(config: Record) { diff --git a/src/plugins/data/common/search/es_search/index.ts b/src/plugins/data/common/search/es_search/index.ts index 7bc9cada8f0ee..54757b53b8665 100644 --- a/src/plugins/data/common/search/es_search/index.ts +++ b/src/plugins/data/common/search/es_search/index.ts @@ -22,4 +22,5 @@ export { IEsSearchRequest, IEsSearchResponse, ES_SEARCH_STRATEGY, + ISearchOptions, } from './types'; diff --git a/src/plugins/data/common/search/es_search/types.ts b/src/plugins/data/common/search/es_search/types.ts index 3184fbe341705..89faa5b7119c8 100644 --- a/src/plugins/data/common/search/es_search/types.ts +++ b/src/plugins/data/common/search/es_search/types.ts @@ -22,6 +22,17 @@ import { IKibanaSearchRequest, IKibanaSearchResponse } from '../types'; export const ES_SEARCH_STRATEGY = 'es'; +export interface ISearchOptions { + /** + * An `AbortSignal` that allows the caller of `search` to abort a search request. + */ + abortSignal?: AbortSignal; + /** + * Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. + */ + strategy?: string; +} + export type ISearchRequestParams> = { trackTotalHits?: boolean; } & Search; diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index d8184551b7f3d..3bfb0ddb89aa9 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -28,4 +28,5 @@ export { IEsSearchResponse, ES_SEARCH_STRATEGY, ISearchRequestParams, + ISearchOptions, } from './es_search'; diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts index a3b9b0b344823..2ad20c3807819 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -30,11 +30,7 @@ import { ValueClickContext } from '../../../../embeddable/public'; const mockField = { name: 'bytes', - indexPattern: { - id: 'logstash-*', - }, filterable: true, - format: new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), }; describe('createFiltersFromValueClick', () => { @@ -81,6 +77,8 @@ describe('createFiltersFromValueClick', () => { getByName: () => mockField, filter: () => [mockField], }, + getFormatterForField: () => + new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), }), } as unknown) as IndexPatternsContract); }); diff --git a/src/test_utils/public/stub_field_formats.ts b/src/plugins/data/public/field_formats/field_formats_registry.stub.ts similarity index 81% rename from src/test_utils/public/stub_field_formats.ts rename to src/plugins/data/public/field_formats/field_formats_registry.stub.ts index 589e93fd600c2..e8741ca41036b 100644 --- a/src/test_utils/public/stub_field_formats.ts +++ b/src/plugins/data/public/field_formats/field_formats_registry.stub.ts @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { CoreSetup } from 'kibana/public'; -import { DataPublicPluginStart, fieldFormats } from '../../plugins/data/public'; -import { deserializeFieldFormat } from '../../plugins/data/public/field_formats/utils/deserialize'; -import { baseFormattersPublic } from '../../plugins/data/public'; + +import { CoreSetup } from 'src/core/public'; +import { deserializeFieldFormat } from './utils/deserialize'; +import { baseFormattersPublic } from './constants'; +import { DataPublicPluginStart, fieldFormats } from '..'; export const getFieldFormatsRegistry = (core: CoreSetup) => { const fieldFormatsRegistry = new fieldFormats.FieldFormatsRegistry(); diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 27b16c57ffecf..f7b4111df5172 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -262,7 +262,7 @@ export { UI_SETTINGS, TypeMeta as IndexPatternTypeMeta, AggregationRestrictions as IndexPatternAggRestrictions, - FieldList, + fieldList, } from '../common'; /* @@ -342,7 +342,6 @@ export { ES_SEARCH_STRATEGY, EsQuerySortValue, extractSearchSourceReferences, - FetchOptions, getEsPreference, getSearchParamsFromRequest, IEsSearchRequest, @@ -352,7 +351,6 @@ export { injectSearchSourceReferences, ISearch, ISearchGeneric, - ISearchOptions, ISearchSource, parseSearchSourceJSON, RequestTimeoutError, @@ -367,6 +365,8 @@ export { EsRawResponseExpressionTypeDefinition, } from './search'; +export { ISearchOptions } from '../common'; + // Search namespace export const search = { aggs: { diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts index 5e0eeaee3c0d0..f1f5f8737b389 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts @@ -17,7 +17,7 @@ * under the License. */ -import { setup } from 'test_utils/http_test_setup'; +import { setup } from '../../../../../core/test_helpers/http_test_setup'; export const { http } = setup((injectedMetadata) => { injectedMetadata.getBasePath.mockReturnValue('/hola/daro/'); diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 3bc19a578a417..3b18e0fbed537 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -19,13 +19,7 @@ import './index.scss'; -import { - PluginInitializerContext, - CoreSetup, - CoreStart, - Plugin, - PackageInfo, -} from 'src/core/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { ConfigSchema } from '../config'; import { Storage, IStorageWrapper, createStartServicesGetter } from '../../kibana_utils/public'; import { @@ -100,7 +94,6 @@ export class DataPublicPlugin private readonly fieldFormatsService: FieldFormatsService; private readonly queryService: QueryService; private readonly storage: IStorageWrapper; - private readonly packageInfo: PackageInfo; constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(); @@ -108,7 +101,6 @@ export class DataPublicPlugin this.fieldFormatsService = new FieldFormatsService(); this.autocomplete = new AutocompleteService(initializerContext); this.storage = new Storage(window.localStorage); - this.packageInfo = initializerContext.env.packageInfo; } public setup( @@ -145,7 +137,6 @@ export class DataPublicPlugin const searchService = this.searchService.setup(core, { usageCollection, - packageInfo: this.packageInfo, expressions, }); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 0c4465ae7f4b9..b7e7e81ae2cef 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -26,11 +26,12 @@ import { EuiGlobalToastListToast } from '@elastic/eui'; import { ExclusiveUnion } from '@elastic/eui'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; -import { FetchOptions as FetchOptions_2 } from 'src/plugins/data/public'; import { History } from 'history'; import { Href } from 'history'; +import { HttpStart } from 'src/core/public'; import { IconType } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n/react'; +import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource as ISearchSource_2 } from 'src/plugins/data/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IUiSettingsClient } from 'src/core/public'; @@ -486,16 +487,6 @@ export const extractSearchSourceReferences: (state: SearchSourceFields) => [Sear indexRefName?: string; }, SavedObjectReference[]]; -// Warning: (ae-missing-release-tag) "FetchOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface FetchOptions { - // (undocumented) - abortSignal?: AbortSignal; - // (undocumented) - searchStrategyId?: string; -} - // Warning: (ae-missing-release-tag) "FieldFormat" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -604,46 +595,11 @@ export type FieldFormatsContentType = 'html' | 'text'; // @public (undocumented) export type FieldFormatsGetConfigFn = GetConfigFn; -// Warning: (ae-missing-release-tag) "FieldList" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "fieldList" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class FieldList extends Array implements IIndexPatternFieldList { - // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "OnNotification" needs to be exported by the entry point index.d.ts - constructor(indexPattern: IndexPattern, specs?: FieldSpec[], shortDotsEnable?: boolean, onNotification?: OnNotification); - // (undocumented) - readonly add: (field: FieldSpec) => void; - // (undocumented) - readonly getAll: () => IndexPatternField[]; - // (undocumented) - readonly getByName: (name: IndexPatternField['name']) => IndexPatternField | undefined; - // (undocumented) - readonly getByType: (type: IndexPatternField['type']) => any[]; - // (undocumented) - readonly remove: (field: IFieldType) => void; - // (undocumented) - readonly removeAll: () => void; - // (undocumented) - readonly replaceAll: (specs: FieldSpec[]) => void; - // (undocumented) - readonly toSpec: () => { - count: number; - script: string | undefined; - lang: string | undefined; - conflictDescriptions: Record | undefined; - name: string; - type: string; - esTypes: string[] | undefined; - scripted: boolean; - searchable: boolean; - aggregatable: boolean; - readFromDocValues: boolean; - subType: import("../types").IFieldSubType | undefined; - format: any; - }[]; - // (undocumented) - readonly update: (field: FieldSpec) => void; -} +export const fieldList: (specs?: FieldSpec[], shortDotsEnable?: boolean) => IIndexPatternFieldList; // @public (undocumented) export interface FieldMappingSpec { @@ -868,7 +824,9 @@ export interface IFieldType { // (undocumented) subType?: IFieldSubType; // (undocumented) - toSpec?: () => FieldSpec; + toSpec?: (options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }) => FieldSpec; // (undocumented) type: string; // (undocumented) @@ -919,6 +877,10 @@ export interface IIndexPatternFieldList extends Array { // (undocumented) replaceAll(specs: FieldSpec[]): void; // (undocumented) + toSpec(options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }): FieldSpec[]; + // (undocumented) update(field: FieldSpec): void; } @@ -1051,12 +1013,8 @@ export class IndexPattern implements IIndexPattern { // (undocumented) title: string; // (undocumented) - toJSON(): string | undefined; - // (undocumented) toSpec(): IndexPatternSpec; // (undocumented) - toString(): string; - // (undocumented) type: string | undefined; // (undocumented) typeMeta?: IndexPatternTypeMeta; @@ -1100,7 +1058,7 @@ export interface IndexPatternAttributes { // // @public (undocumented) export class IndexPatternField implements IFieldType { - constructor(indexPattern: IndexPattern, spec: FieldSpec, displayName: string, onNotification: OnNotification); + constructor(spec: FieldSpec, displayName: string); // (undocumented) get aggregatable(): boolean; // (undocumented) @@ -1116,10 +1074,6 @@ export class IndexPatternField implements IFieldType { // (undocumented) get filterable(): boolean; // (undocumented) - get format(): FieldFormat; - // (undocumented) - readonly indexPattern: IndexPattern; - // (undocumented) get lang(): string | undefined; set lang(lang: string | undefined); // (undocumented) @@ -1155,7 +1109,9 @@ export class IndexPatternField implements IFieldType { subType: import("../types").IFieldSubType | undefined; }; // (undocumented) - toSpec(): { + toSpec({ getFormatterForField, }?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }): { count: number; script: string | undefined; lang: string | undefined; @@ -1168,7 +1124,10 @@ export class IndexPatternField implements IFieldType { aggregatable: boolean; readFromDocValues: boolean; subType: import("../types").IFieldSubType | undefined; - format: any; + format: { + id: any; + params: any; + } | undefined; }; // (undocumented) get type(): string; @@ -1265,9 +1224,7 @@ export type ISearchGeneric = { getByName: () => field, filter: () => [field], }, + getFormatterForField: () => new BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), } as any; return new AggConfigs( diff --git a/src/plugins/data/public/search/expressions/esdsl.ts b/src/plugins/data/public/search/expressions/esdsl.ts index d7b897ace29b4..2efb21671b5b7 100644 --- a/src/plugins/data/public/search/expressions/esdsl.ts +++ b/src/plugins/data/public/search/expressions/esdsl.ts @@ -140,7 +140,7 @@ export const esdsl = (): EsdslExpressionFunctionDefinition => ({ body: dsl, }, }, - { signal: abortSignal } + { abortSignal } ) .toPromise(); diff --git a/src/plugins/data/public/search/fetch/types.ts b/src/plugins/data/public/search/fetch/types.ts index 670c4f731971a..224a597766909 100644 --- a/src/plugins/data/public/search/fetch/types.ts +++ b/src/plugins/data/public/search/fetch/types.ts @@ -17,8 +17,9 @@ * under the License. */ +import { HttpStart } from 'src/core/public'; +import { BehaviorSubject } from 'rxjs'; import { GetConfigFn } from '../../../common'; -import { ISearchStartLegacy } from '../types'; /** * @internal @@ -29,15 +30,10 @@ import { ISearchStartLegacy } from '../types'; */ export type SearchRequest = Record; -export interface FetchOptions { - abortSignal?: AbortSignal; - searchStrategyId?: string; -} - export interface FetchHandlers { - legacySearchService: ISearchStartLegacy; config: { get: GetConfigFn }; - esShardTimeout: number; + http: HttpStart; + loadingCount$: BehaviorSubject; } export interface SearchError { diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 14eff13b378ee..a6a1736ac91da 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -19,14 +19,7 @@ export * from './expressions'; -export { - ISearch, - ISearchOptions, - ISearchGeneric, - ISearchSetup, - ISearchStart, - SearchEnhancements, -} from './types'; +export { ISearch, ISearchGeneric, ISearchSetup, ISearchStart, SearchEnhancements } from './types'; export { IEsSearchResponse, IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../common/search'; @@ -34,7 +27,7 @@ export { getEsPreference } from './es_search'; export { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search'; -export { SearchError, FetchOptions, getSearchParamsFromRequest, SearchRequest } from './fetch'; +export { SearchError, getSearchParamsFromRequest, SearchRequest } from './fetch'; export { ISearchSource, diff --git a/src/plugins/data/public/search/legacy/call_client.test.ts b/src/plugins/data/public/search/legacy/call_client.test.ts index a3c4e720b4cab..38f3ab200da90 100644 --- a/src/plugins/data/public/search/legacy/call_client.test.ts +++ b/src/plugins/data/public/search/legacy/call_client.test.ts @@ -17,11 +17,13 @@ * under the License. */ +import { coreMock } from '../../../../../core/public/mocks'; import { callClient } from './call_client'; import { SearchStrategySearchParams } from './types'; import { defaultSearchStrategy } from './default_search_strategy'; import { FetchHandlers } from '../fetch'; import { handleResponse } from '../fetch/handle_response'; +import { BehaviorSubject } from 'rxjs'; const mockAbortFn = jest.fn(); jest.mock('../fetch/handle_response', () => ({ @@ -54,7 +56,13 @@ describe('callClient', () => { test('Passes the additional arguments it is given to the search strategy', () => { const searchRequests = [{ _searchStrategyId: 0 }]; - const args = { legacySearchService: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; + const args = { + http: coreMock.createStart().http, + legacySearchService: {}, + config: { get: jest.fn() }, + esShardTimeout: 0, + loadingCount$: new BehaviorSubject(0), + } as FetchHandlers; callClient(searchRequests, [], args); diff --git a/src/plugins/data/public/search/legacy/call_client.ts b/src/plugins/data/public/search/legacy/call_client.ts index 3dcf11f72a742..d66796b9427a1 100644 --- a/src/plugins/data/public/search/legacy/call_client.ts +++ b/src/plugins/data/public/search/legacy/call_client.ts @@ -18,21 +18,22 @@ */ import { SearchResponse } from 'elasticsearch'; -import { FetchOptions, FetchHandlers, handleResponse } from '../fetch'; +import { ISearchOptions } from 'src/plugins/data/common'; +import { FetchHandlers, handleResponse } from '../fetch'; import { defaultSearchStrategy } from './default_search_strategy'; import { SearchRequest } from '../index'; export function callClient( searchRequests: SearchRequest[], - requestsOptions: FetchOptions[] = [], + requestsOptions: ISearchOptions[] = [], fetchHandlers: FetchHandlers ) { // Correlate the options with the request that they're associated with const requestOptionEntries: Array<[ SearchRequest, - FetchOptions + ISearchOptions ]> = searchRequests.map((request, i) => [request, requestsOptions[i]]); - const requestOptionsMap = new Map(requestOptionEntries); + const requestOptionsMap = new Map(requestOptionEntries); const requestResponseMap = new Map>>(); const { searching, abort } = defaultSearchStrategy.search({ diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts b/src/plugins/data/public/search/legacy/default_search_strategy.test.ts index 4148055c1eb58..e74ab49131430 100644 --- a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts +++ b/src/plugins/data/public/search/legacy/default_search_strategy.test.ts @@ -17,44 +17,26 @@ * under the License. */ -import { IUiSettingsClient } from 'kibana/public'; +import { HttpStart } from 'src/core/public'; +import { coreMock } from '../../../../../core/public/mocks'; import { defaultSearchStrategy } from './default_search_strategy'; -import { searchServiceMock } from '../mocks'; import { SearchStrategySearchParams } from './types'; -import { UI_SETTINGS } from '../../../common'; +import { BehaviorSubject } from 'rxjs'; const { search } = defaultSearchStrategy; -function getConfigStub(config: any = {}) { - return { - get: (key) => config[key], - } as IUiSettingsClient; -} - -const msearchMockResponse: any = Promise.resolve([]); -msearchMockResponse.abort = jest.fn(); -const msearchMock = jest.fn().mockReturnValue(msearchMockResponse); - -const searchMockResponse: any = Promise.resolve([]); -searchMockResponse.abort = jest.fn(); -const searchMock = jest.fn().mockReturnValue(searchMockResponse); +const msearchMock = jest.fn().mockResolvedValue({ body: { responses: [] } }); describe('defaultSearchStrategy', function () { describe('search', function () { - let searchArgs: MockedKeys>; - let es: any; + let searchArgs: MockedKeys; + let http: jest.Mocked; beforeEach(() => { - msearchMockResponse.abort.mockClear(); msearchMock.mockClear(); - searchMockResponse.abort.mockClear(); - searchMock.mockClear(); - - const searchService = searchServiceMock.createStartContract(); - searchService.aggs.calculateAutoTimeExpression = jest.fn().mockReturnValue('1d'); - searchService.__LEGACY.esClient.search = searchMock; - searchService.__LEGACY.esClient.msearch = msearchMock; + http = coreMock.createStart().http; + http.post.mockResolvedValue(msearchMock); searchArgs = { searchRequests: [ @@ -62,49 +44,27 @@ describe('defaultSearchStrategy', function () { index: { title: 'foo' }, }, ], - esShardTimeout: 0, - legacySearchService: searchService.__LEGACY, + http, + config: { + get: jest.fn(), + }, + loadingCount$: new BehaviorSubject(0) as any, }; - - es = searchArgs.legacySearchService.esClient; - }); - - test('does not send max_concurrent_shard_requests by default', async () => { - const config = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); - await search({ ...searchArgs, config }); - expect(es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(undefined); - }); - - test('allows configuration of max_concurrent_shard_requests', async () => { - const config = getConfigStub({ - [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, - [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 42, - }); - await search({ ...searchArgs, config }); - expect(es.msearch.mock.calls[0][0].max_concurrent_shard_requests).toBe(42); - }); - - test('should set rest_total_hits_as_int to true on a request', async () => { - const config = getConfigStub({ [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true }); - await search({ ...searchArgs, config }); - expect(es.msearch.mock.calls[0][0]).toHaveProperty('rest_total_hits_as_int', true); - }); - - test('should set ignore_throttled=false when including frozen indices', async () => { - const config = getConfigStub({ - [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, - [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: true, - }); - await search({ ...searchArgs, config }); - expect(es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); }); - test('should properly call abort with msearch', () => { - const config = getConfigStub({ - [UI_SETTINGS.COURIER_BATCH_SEARCHES]: true, - }); - search({ ...searchArgs, config }).abort(); - expect(msearchMockResponse.abort).toHaveBeenCalled(); + test('calls http.post with the correct arguments', async () => { + await search({ ...searchArgs }); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/internal/_msearch", + Object { + "body": "{\\"searches\\":[{\\"header\\":{\\"index\\":\\"foo\\"}}]}", + "signal": AbortSignal {}, + }, + ], + ] + `); }); }); }); diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.ts b/src/plugins/data/public/search/legacy/default_search_strategy.ts index 6ccb0a01cf898..cbcd0da20207f 100644 --- a/src/plugins/data/public/search/legacy/default_search_strategy.ts +++ b/src/plugins/data/public/search/legacy/default_search_strategy.ts @@ -17,8 +17,7 @@ * under the License. */ -import { getPreference, getTimeout } from '../fetch'; -import { getMSearchParams } from './get_msearch_params'; +import { getPreference } from '../fetch'; import { SearchStrategyProvider, SearchStrategySearchParams } from './types'; // @deprecated @@ -30,34 +29,45 @@ export const defaultSearchStrategy: SearchStrategyProvider = { }, }; -function msearch({ - searchRequests, - legacySearchService, - config, - esShardTimeout, -}: SearchStrategySearchParams) { - const es = legacySearchService.esClient; - const inlineRequests = searchRequests.map(({ index, body, search_type: searchType }) => { - const inlineHeader = { - index: index.title || index, - search_type: searchType, - ignore_unavailable: true, - preference: getPreference(config.get), +function msearch({ searchRequests, config, http, loadingCount$ }: SearchStrategySearchParams) { + const requests = searchRequests.map(({ index, body }) => { + return { + header: { + index: index.title || index, + preference: getPreference(config.get), + }, + body, }; - const inlineBody = { - ...body, - timeout: getTimeout(esShardTimeout), - }; - return `${JSON.stringify(inlineHeader)}\n${JSON.stringify(inlineBody)}`; }); - const searching = es.msearch({ - ...getMSearchParams(config.get), - body: `${inlineRequests.join('\n')}\n`, - }); + const abortController = new AbortController(); + let resolved = false; + + // Start LoadingIndicator + loadingCount$.next(loadingCount$.getValue() + 1); + + const cleanup = () => { + if (!resolved) { + resolved = true; + // Decrement loading counter & cleanup BehaviorSubject + loadingCount$.next(loadingCount$.getValue() - 1); + loadingCount$.complete(); + } + }; + + const searching = http + .post('/internal/_msearch', { + body: JSON.stringify({ searches: requests }), + signal: abortController.signal, + }) + .then(({ body }) => body?.responses) + .finally(() => cleanup()); return { - searching: searching.then(({ responses }: any) => responses), - abort: searching.abort, + abort: () => { + abortController.abort(); + cleanup(); + }, + searching, }; } diff --git a/src/plugins/data/public/search/legacy/es_client/get_es_client.ts b/src/plugins/data/public/search/legacy/es_client/get_es_client.ts deleted file mode 100644 index 4367544ad9ff4..0000000000000 --- a/src/plugins/data/public/search/legacy/es_client/get_es_client.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// @ts-ignore -import { default as es } from 'elasticsearch-browser/elasticsearch'; -import { CoreStart, PackageInfo } from 'kibana/public'; -import { BehaviorSubject } from 'rxjs'; - -export function getEsClient({ - esRequestTimeout, - esApiVersion, - http, - packageVersion, -}: { - esRequestTimeout: number; - esApiVersion: string; - http: CoreStart['http']; - packageVersion: PackageInfo['version']; -}) { - // Use legacy es client for msearch. - const client = es.Client({ - host: getEsUrl(http, packageVersion), - log: 'info', - requestTimeout: esRequestTimeout, - apiVersion: esApiVersion, - }); - - const loadingCount$ = new BehaviorSubject(0); - http.addLoadingCountSource(loadingCount$); - - return { - search: wrapEsClientMethod(client, 'search', loadingCount$), - msearch: wrapEsClientMethod(client, 'msearch', loadingCount$), - create: wrapEsClientMethod(client, 'create', loadingCount$), - }; -} - -function wrapEsClientMethod(esClient: any, method: string, loadingCount$: BehaviorSubject) { - return (args: any) => { - // esClient returns a promise, with an additional abort handler - // To tap into the abort handling, we have to override that abort handler. - const customPromiseThingy = esClient[method](args); - const { abort } = customPromiseThingy; - let resolved = false; - - // Start LoadingIndicator - loadingCount$.next(loadingCount$.getValue() + 1); - - // Stop LoadingIndicator when user aborts - customPromiseThingy.abort = () => { - abort(); - if (!resolved) { - resolved = true; - loadingCount$.next(loadingCount$.getValue() - 1); - } - }; - - // Stop LoadingIndicator when promise finishes - customPromiseThingy.finally(() => { - resolved = true; - loadingCount$.next(loadingCount$.getValue() - 1); - }); - - return customPromiseThingy; - }; -} - -function getEsUrl(http: CoreStart['http'], packageVersion: PackageInfo['version']) { - const a = document.createElement('a'); - a.href = http.basePath.prepend('/elasticsearch'); - const protocolPort = /https/.test(a.protocol) ? 443 : 80; - const port = a.port || protocolPort; - return { - host: a.hostname, - port, - protocol: a.protocol, - pathname: a.pathname, - headers: { - 'kbn-version': packageVersion, - }, - }; -} diff --git a/src/plugins/data/public/search/legacy/fetch_soon.test.ts b/src/plugins/data/public/search/legacy/fetch_soon.test.ts index d7a85e65b475d..d38a41cf5ffbc 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.test.ts +++ b/src/plugins/data/public/search/legacy/fetch_soon.test.ts @@ -19,10 +19,10 @@ import { fetchSoon } from './fetch_soon'; import { callClient } from './call_client'; -import { FetchHandlers, FetchOptions } from '../fetch/types'; +import { FetchHandlers } from '../fetch/types'; import { SearchRequest } from '../index'; import { SearchResponse } from 'elasticsearch'; -import { GetConfigFn, UI_SETTINGS } from '../../../common'; +import { GetConfigFn, UI_SETTINGS, ISearchOptions } from '../../../common'; function getConfigStub(config: any = {}): GetConfigFn { return (key) => config[key]; @@ -102,7 +102,7 @@ describe('fetchSoon', () => { const options = [{ bar: 1 }, { bar: 2 }]; requests.forEach((request, i) => { - fetchSoon(request, options[i] as FetchOptions, { config } as FetchHandlers); + fetchSoon(request, options[i] as ISearchOptions, { config } as FetchHandlers); }); jest.advanceTimersByTime(50); diff --git a/src/plugins/data/public/search/legacy/fetch_soon.ts b/src/plugins/data/public/search/legacy/fetch_soon.ts index 16920a8a4dd97..37c3827bb7bba 100644 --- a/src/plugins/data/public/search/legacy/fetch_soon.ts +++ b/src/plugins/data/public/search/legacy/fetch_soon.ts @@ -19,9 +19,9 @@ import { SearchResponse } from 'elasticsearch'; import { callClient } from './call_client'; -import { FetchHandlers, FetchOptions } from '../fetch/types'; +import { FetchHandlers } from '../fetch/types'; import { SearchRequest } from '../index'; -import { UI_SETTINGS } from '../../../common'; +import { UI_SETTINGS, ISearchOptions } from '../../../common'; /** * This function introduces a slight delay in the request process to allow multiple requests to queue @@ -29,7 +29,7 @@ import { UI_SETTINGS } from '../../../common'; */ export async function fetchSoon( request: SearchRequest, - options: FetchOptions, + options: ISearchOptions, fetchHandlers: FetchHandlers ) { const msToDelay = fetchHandlers.config.get(UI_SETTINGS.COURIER_BATCH_SEARCHES) ? 50 : 0; @@ -51,7 +51,7 @@ function delay(fn: (...args: any) => T, ms: number): Promise { // The current batch/queue of requests to fetch let requestsToFetch: SearchRequest[] = []; -let requestOptions: FetchOptions[] = []; +let requestOptions: ISearchOptions[] = []; // The in-progress fetch (if there is one) let fetchInProgress: any = null; @@ -65,7 +65,7 @@ let fetchInProgress: any = null; */ async function delayedFetch( request: SearchRequest, - options: FetchOptions, + options: ISearchOptions, fetchHandlers: FetchHandlers, ms: number ): Promise> { diff --git a/src/plugins/data/public/search/legacy/get_msearch_params.test.ts b/src/plugins/data/public/search/legacy/get_msearch_params.test.ts deleted file mode 100644 index d3206950174c8..0000000000000 --- a/src/plugins/data/public/search/legacy/get_msearch_params.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { getMSearchParams } from './get_msearch_params'; -import { GetConfigFn, UI_SETTINGS } from '../../../common'; - -function getConfigStub(config: any = {}): GetConfigFn { - return (key) => config[key]; -} - -describe('getMSearchParams', () => { - test('includes rest_total_hits_as_int', () => { - const config = getConfigStub(); - const msearchParams = getMSearchParams(config); - expect(msearchParams.rest_total_hits_as_int).toBe(true); - }); - - test('includes ignore_throttled according to search:includeFrozen', () => { - let config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: true }); - let msearchParams = getMSearchParams(config); - expect(msearchParams.ignore_throttled).toBe(false); - - config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false }); - msearchParams = getMSearchParams(config); - expect(msearchParams.ignore_throttled).toBe(true); - }); - - test('includes max_concurrent_shard_requests according to courier:maxConcurrentShardRequests if greater than 0', () => { - let config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 0 }); - let msearchParams = getMSearchParams(config); - expect(msearchParams.max_concurrent_shard_requests).toBe(undefined); - - config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 5 }); - msearchParams = getMSearchParams(config); - expect(msearchParams.max_concurrent_shard_requests).toBe(5); - }); - - test('does not include other search params that are included in the msearch header or body', () => { - const config = getConfigStub({ - [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, - [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 5, - }); - const msearchParams = getMSearchParams(config); - expect(msearchParams.hasOwnProperty('ignore_unavailable')).toBe(false); - expect(msearchParams.hasOwnProperty('preference')).toBe(false); - expect(msearchParams.hasOwnProperty('timeout')).toBe(false); - }); -}); diff --git a/src/plugins/data/public/search/legacy/get_msearch_params.ts b/src/plugins/data/public/search/legacy/get_msearch_params.ts deleted file mode 100644 index c4f77b25078cd..0000000000000 --- a/src/plugins/data/public/search/legacy/get_msearch_params.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { GetConfigFn } from '../../../common'; -import { getIgnoreThrottled, getMaxConcurrentShardRequests } from '../fetch'; - -export function getMSearchParams(getConfig: GetConfigFn) { - return { - rest_total_hits_as_int: true, - ignore_throttled: getIgnoreThrottled(getConfig), - max_concurrent_shard_requests: getMaxConcurrentShardRequests(getConfig), - }; -} diff --git a/src/plugins/data/public/search/legacy/index.ts b/src/plugins/data/public/search/legacy/index.ts index e2ae72824f3f4..74e516f407e8c 100644 --- a/src/plugins/data/public/search/legacy/index.ts +++ b/src/plugins/data/public/search/legacy/index.ts @@ -18,4 +18,3 @@ */ export { fetchSoon } from './fetch_soon'; -export { getEsClient, LegacyApiCaller } from './es_client'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index 8ccf46fe7c97d..f4ed7d8b122b9 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -35,12 +35,6 @@ function createStartContract(): jest.Mocked { aggs: searchAggsStartMock(), search: jest.fn(), searchSource: searchSourceMock, - __LEGACY: { - esClient: { - search: jest.fn(), - msearch: jest.fn(), - }, - }, }; } diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index 2eded17bda88c..da60f39b522ac 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -112,7 +112,9 @@ describe('SearchInterceptor', () => { const mockRequest: IEsSearchRequest = { params: {}, }; - const response = searchInterceptor.search(mockRequest, { signal: abortController.signal }); + const response = searchInterceptor.search(mockRequest, { + abortSignal: abortController.signal, + }); const next = jest.fn(); const error = (e: any) => { @@ -131,7 +133,7 @@ describe('SearchInterceptor', () => { const mockRequest: IEsSearchRequest = { params: {}, }; - const response = searchInterceptor.search(mockRequest, { signal: abort.signal }); + const response = searchInterceptor.search(mockRequest, { abortSignal: abort.signal }); abort.abort(); const error = (e: any) => { diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 30e509edd4987..c6c03267163c9 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -22,8 +22,12 @@ import { BehaviorSubject, throwError, timer, Subscription, defer, from, Observab import { finalize, filter } from 'rxjs/operators'; import { Toast, CoreStart, ToastsSetup, CoreSetup } from 'kibana/public'; import { getCombinedSignal, AbortError } from '../../common/utils'; -import { IEsSearchRequest, IEsSearchResponse, ES_SEARCH_STRATEGY } from '../../common/search'; -import { ISearchOptions } from './types'; +import { + IEsSearchRequest, + IEsSearchResponse, + ISearchOptions, + ES_SEARCH_STRATEGY, +} from '../../common/search'; import { getLongQueryNotification } from './long_query_notification'; import { SearchUsageCollector } from './collectors'; @@ -128,7 +132,7 @@ export class SearchInterceptor { ): Observable { // Defer the following logic until `subscribe` is actually called return defer(() => { - if (options?.signal?.aborted) { + if (options?.abortSignal?.aborted) { return throwError(new AbortError()); } @@ -164,7 +168,7 @@ export class SearchInterceptor { const signals = [ this.abortController.signal, timeoutSignal, - ...(options?.signal ? [options.signal] : []), + ...(options?.abortSignal ? [options.abortSignal] : []), ]; const combinedSignal = getCombinedSignal(signals); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 9a30a15936fe5..a49d2ef0956ff 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -17,11 +17,11 @@ * under the License. */ -import { Plugin, CoreSetup, CoreStart, PackageInfo } from 'src/core/public'; +import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; +import { BehaviorSubject } from 'rxjs'; import { ISearchSetup, ISearchStart, SearchEnhancements } from './types'; import { createSearchSource, SearchSource, SearchSourceDependencies } from './search_source'; -import { getEsClient, LegacyApiCaller } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; import { IndexPatternsContract } from '../index_patterns/index_patterns'; import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; @@ -33,9 +33,8 @@ import { ExpressionsSetup } from '../../../expressions/public'; /** @internal */ export interface SearchServiceSetupDependencies { - packageInfo: PackageInfo; - usageCollection?: UsageCollectionSetup; expressions: ExpressionsSetup; + usageCollection?: UsageCollectionSetup; } /** @internal */ @@ -45,28 +44,18 @@ export interface SearchServiceStartDependencies { } export class SearchService implements Plugin { - private esClient?: LegacyApiCaller; private readonly aggsService = new AggsService(); private searchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; public setup( { http, getStartServices, injectedMetadata, notifications, uiSettings }: CoreSetup, - { expressions, packageInfo, usageCollection }: SearchServiceSetupDependencies + { expressions, usageCollection }: SearchServiceSetupDependencies ): ISearchSetup { - const esApiVersion = injectedMetadata.getInjectedVar('esApiVersion') as string; const esRequestTimeout = injectedMetadata.getInjectedVar('esRequestTimeout') as number; - const packageVersion = packageInfo.version; this.usageCollector = createUsageCollector(getStartServices, usageCollection); - this.esClient = getEsClient({ - esRequestTimeout, - esApiVersion, - http, - packageVersion, - }); - /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -107,15 +96,16 @@ export class SearchService implements Plugin { return this.searchInterceptor.search(request, options); }) as ISearchGeneric; - const legacySearch = { - esClient: this.esClient!, - }; + const loadingCount$ = new BehaviorSubject(0); + http.addLoadingCountSource(loadingCount$); const searchSourceDependencies: SearchSourceDependencies = { getConfig: uiSettings.get.bind(uiSettings), + // TODO: we don't need this, apply on the server esShardTimeout: injectedMetadata.getInjectedVar('esShardTimeout') as number, search, - legacySearch, + http, + loadingCount$, }; return { @@ -127,7 +117,6 @@ export class SearchService implements Plugin { return new SearchSource({}, searchSourceDependencies); }, }, - __LEGACY: legacySearch, }; } diff --git a/src/plugins/data/public/search/search_source/create_search_source.test.ts b/src/plugins/data/public/search/search_source/create_search_source.test.ts index 56f6ca6c56270..2820aab67ea3a 100644 --- a/src/plugins/data/public/search/search_source/create_search_source.test.ts +++ b/src/plugins/data/public/search/search_source/create_search_source.test.ts @@ -22,7 +22,8 @@ import { SearchSourceDependencies } from './search_source'; import { IIndexPattern } from '../../../common/index_patterns'; import { IndexPatternsContract } from '../../index_patterns/index_patterns'; import { Filter } from '../../../common/es_query/filters'; -import { dataPluginMock } from '../../mocks'; +import { coreMock } from '../../../../../core/public/mocks'; +import { BehaviorSubject } from 'rxjs'; describe('createSearchSource', () => { const indexPatternMock: IIndexPattern = {} as IIndexPattern; @@ -31,13 +32,12 @@ describe('createSearchSource', () => { let createSearchSource: ReturnType; beforeEach(() => { - const data = dataPluginMock.createStartContract(); - dependencies = { getConfig: jest.fn(), search: jest.fn(), - legacySearch: data.search.__LEGACY, esShardTimeout: 30000, + http: coreMock.createStart().http, + loadingCount$: new BehaviorSubject(0), }; indexPatternContractMock = ({ diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/public/search/search_source/mocks.ts index 4e1c35557ffa6..bc3e287d9fe80 100644 --- a/src/plugins/data/public/search/search_source/mocks.ts +++ b/src/plugins/data/public/search/search_source/mocks.ts @@ -17,7 +17,8 @@ * under the License. */ -import { uiSettingsServiceMock } from '../../../../../core/public/mocks'; +import { BehaviorSubject } from 'rxjs'; +import { httpServiceMock, uiSettingsServiceMock } from '../../../../../core/public/mocks'; import { ISearchSource, SearchSource } from './search_source'; import { SearchSourceFields } from './types'; @@ -54,10 +55,6 @@ export const createSearchSourceMock = (fields?: SearchSourceFields) => getConfig: uiSettingsServiceMock.createStartContract().get, esShardTimeout: 30000, search: jest.fn(), - legacySearch: { - esClient: { - search: jest.fn(), - msearch: jest.fn(), - }, - }, + http: httpServiceMock.createStartContract(), + loadingCount$: new BehaviorSubject(0), }); diff --git a/src/plugins/data/public/search/search_source/search_source.test.ts b/src/plugins/data/public/search/search_source/search_source.test.ts index 2f0fa0765e25a..a8baed9faa84d 100644 --- a/src/plugins/data/public/search/search_source/search_source.test.ts +++ b/src/plugins/data/public/search/search_source/search_source.test.ts @@ -17,12 +17,12 @@ * under the License. */ -import { Observable } from 'rxjs'; +import { Observable, BehaviorSubject } from 'rxjs'; import { GetConfigFn } from 'src/plugins/data/common'; import { SearchSource, SearchSourceDependencies } from './search_source'; import { IndexPattern, SortDirection } from '../..'; import { fetchSoon } from '../legacy'; -import { dataPluginMock } from '../../../../data/public/mocks'; +import { coreMock } from '../../../../../core/public/mocks'; jest.mock('../legacy', () => ({ fetchSoon: jest.fn().mockResolvedValue({}), @@ -54,8 +54,6 @@ describe('SearchSource', () => { let searchSourceDependencies: SearchSourceDependencies; beforeEach(() => { - const data = dataPluginMock.createStartContract(); - mockSearchMethod = jest.fn(() => { return new Observable((subscriber) => { setTimeout(() => { @@ -70,8 +68,9 @@ describe('SearchSource', () => { searchSourceDependencies = { getConfig: jest.fn(), search: mockSearchMethod, - legacySearch: data.search.__LEGACY, esShardTimeout: 30000, + http: coreMock.createStart().http, + loadingCount$: new BehaviorSubject(0), }; }); diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index d2e3370762059..eec2d9b50eafe 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -72,25 +72,31 @@ import { setWith } from '@elastic/safer-lodash-set'; import { uniqueId, uniq, extend, pick, difference, omit, isObject, keys, isFunction } from 'lodash'; import { map } from 'rxjs/operators'; +import { HttpStart } from 'src/core/public'; +import { BehaviorSubject } from 'rxjs'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; import { IIndexPattern, ISearchGeneric } from '../..'; import { SearchSourceOptions, SearchSourceFields } from './types'; import { - FetchOptions, RequestFailure, handleResponse, getSearchParamsFromRequest, SearchRequest, } from '../fetch'; -import { getEsQueryConfig, buildEsQuery, Filter, UI_SETTINGS } from '../../../common'; +import { + getEsQueryConfig, + buildEsQuery, + Filter, + UI_SETTINGS, + ISearchOptions, +} from '../../../common'; import { getHighlightRequest } from '../../../common/field_formats'; import { GetConfigFn } from '../../../common/types'; import { fetchSoon } from '../legacy'; import { extractReferences } from './extract_references'; -import { ISearchStartLegacy } from '../types'; /** @internal */ export const searchSourceRequiredUiSettings = [ @@ -111,8 +117,9 @@ export const searchSourceRequiredUiSettings = [ export interface SearchSourceDependencies { getConfig: GetConfigFn; search: ISearchGeneric; - legacySearch: ISearchStartLegacy; + http: HttpStart; esShardTimeout: number; + loadingCount$: BehaviorSubject; } /** @public **/ @@ -121,7 +128,7 @@ export class SearchSource { private searchStrategyId?: string; private parent?: SearchSource; private requestStartHandlers: Array< - (searchSource: SearchSource, options?: FetchOptions) => Promise + (searchSource: SearchSource, options?: ISearchOptions) => Promise > = []; private inheritOptions: SearchSourceOptions = {}; public history: SearchRequest[] = []; @@ -225,7 +232,7 @@ export class SearchSource { * Run a search using the search service * @return {Observable>} */ - private fetch$(searchRequest: SearchRequest, signal?: AbortSignal) { + private fetch$(searchRequest: SearchRequest, options: ISearchOptions) { const { search, esShardTimeout, getConfig } = this.dependencies; const params = getSearchParamsFromRequest(searchRequest, { @@ -233,7 +240,7 @@ export class SearchSource { getConfig, }); - return search({ params, indexType: searchRequest.indexType }, { signal }).pipe( + return search({ params, indexType: searchRequest.indexType }, options).pipe( map(({ rawResponse }) => handleResponse(searchRequest, rawResponse)) ); } @@ -242,8 +249,8 @@ export class SearchSource { * Run a search using the search service * @return {Promise>} */ - private async legacyFetch(searchRequest: SearchRequest, options: FetchOptions) { - const { esShardTimeout, legacySearch, getConfig } = this.dependencies; + private async legacyFetch(searchRequest: SearchRequest, options: ISearchOptions) { + const { http, getConfig, loadingCount$ } = this.dependencies; return await fetchSoon( searchRequest, @@ -252,9 +259,9 @@ export class SearchSource { ...options, }, { - legacySearchService: legacySearch, + http, config: { get: getConfig }, - esShardTimeout, + loadingCount$, } ); } @@ -263,7 +270,7 @@ export class SearchSource { * * @async */ - async fetch(options: FetchOptions = {}) { + async fetch(options: ISearchOptions = {}) { const { getConfig } = this.dependencies; await this.requestIsStarting(options); @@ -274,7 +281,7 @@ export class SearchSource { if (getConfig(UI_SETTINGS.COURIER_BATCH_SEARCHES)) { response = await this.legacyFetch(searchRequest, options); } else { - response = await this.fetch$(searchRequest, options.abortSignal).toPromise(); + response = await this.fetch$(searchRequest, options).toPromise(); } // TODO: Remove casting when https://github.com/elastic/elasticsearch-js/issues/1287 is resolved @@ -291,7 +298,7 @@ export class SearchSource { * @return {undefined} */ onRequestStart( - handler: (searchSource: SearchSource, options?: FetchOptions) => Promise + handler: (searchSource: SearchSource, options?: ISearchOptions) => Promise ) { this.requestStartHandlers.push(handler); } @@ -318,7 +325,7 @@ export class SearchSource { * @param options * @return {Promise} */ - private requestIsStarting(options: FetchOptions = {}) { + private requestIsStarting(options: ISearchOptions = {}) { const handlers = [...this.requestStartHandlers]; // If callParentStartHandlers has been set to true, we also call all // handlers of parent search sources. diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 55726e40f5a77..cec5c63294e96 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -19,7 +19,6 @@ import { Observable } from 'rxjs'; import { PackageInfo } from 'kibana/server'; -import { LegacyApiCaller } from './legacy/es_client'; import { ISearchInterceptor } from './search_interceptor'; import { ISearchSource, SearchSourceFields } from './search_source'; import { SearchUsageCollector } from './collectors'; @@ -29,15 +28,11 @@ import { IKibanaSearchResponse, IEsSearchRequest, IEsSearchResponse, + ISearchOptions, } from '../../common/search'; import { IndexPatternsContract } from '../../common/index_patterns/index_patterns'; import { UsageCollectionSetup } from '../../../usage_collection/public'; -export interface ISearchOptions { - signal?: AbortSignal; - strategy?: string; -} - export type ISearch = ( request: IKibanaSearchRequest, options?: ISearchOptions @@ -51,10 +46,6 @@ export type ISearchGeneric = < options?: ISearchOptions ) => Observable; -export interface ISearchStartLegacy { - esClient: LegacyApiCaller; -} - export interface SearchEnhancements { searchInterceptor: ISearchInterceptor; } @@ -78,11 +69,6 @@ export interface ISearchStart { create: (fields?: SearchSourceFields) => Promise; createEmpty: () => ISearchSource; }; - /** - * @deprecated - * @internal - */ - __LEGACY: ISearchStartLegacy; } export { SEARCH_EVENT_TYPE } from './collectors'; diff --git a/src/legacy/server/server_extensions/index.js b/src/plugins/data/public/test_utils.ts similarity index 90% rename from src/legacy/server/server_extensions/index.js rename to src/plugins/data/public/test_utils.ts index e17bd488897f7..f04b20e96fa1d 100644 --- a/src/legacy/server/server_extensions/index.js +++ b/src/plugins/data/public/test_utils.ts @@ -17,4 +17,4 @@ * under the License. */ -export { serverExtensionsMixin } from './server_extensions_mixin'; +export { getFieldFormatsRegistry } from './field_formats/field_formats_registry.stub'; diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index b86b06d0cd5aa..71ed83290e697 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -198,6 +198,7 @@ export { OptionedValueProp, ParsedInterval, // search + ISearchOptions, IEsSearchRequest, IEsSearchResponse, ES_SEARCH_STRATEGY, @@ -209,7 +210,6 @@ export { export { ISearchStrategy, - ISearchOptions, ISearchSetup, ISearchStart, getDefaultSearchParams, diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 02c21c3254645..8a74c51f52f51 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -17,13 +17,7 @@ * under the License. */ -export { - ISearchStrategy, - ISearchOptions, - ISearchSetup, - ISearchStart, - SearchEnhancements, -} from './types'; +export { ISearchStrategy, ISearchSetup, ISearchStart, SearchEnhancements } from './types'; export { getDefaultSearchParams, getTotalLoaded } from './es_search'; diff --git a/src/legacy/ui/ui_nav_links/index.js b/src/plugins/data/server/search/routes/index.ts similarity index 93% rename from src/legacy/ui/ui_nav_links/index.js rename to src/plugins/data/server/search/routes/index.ts index e725a77ee1183..2217890ff778e 100644 --- a/src/legacy/ui/ui_nav_links/index.js +++ b/src/plugins/data/server/search/routes/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { UiNavLink } from './ui_nav_link'; +export * from './msearch'; +export * from './search'; diff --git a/src/plugins/data/server/search/routes/msearch.test.ts b/src/plugins/data/server/search/routes/msearch.test.ts new file mode 100644 index 0000000000000..0a52cf23c5472 --- /dev/null +++ b/src/plugins/data/server/search/routes/msearch.test.ts @@ -0,0 +1,150 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; + +import { + CoreSetup, + RequestHandlerContext, + SharedGlobalConfig, + StartServicesAccessor, +} from 'src/core/server'; +import { + coreMock, + httpServerMock, + pluginInitializerContextConfigMock, +} from '../../../../../../src/core/server/mocks'; +import { registerMsearchRoute, convertRequestBody } from './msearch'; +import { DataPluginStart } from '../../plugin'; +import { dataPluginMock } from '../../mocks'; + +describe('msearch route', () => { + let mockDataStart: MockedKeys; + let mockCoreSetup: MockedKeys>; + let getStartServices: jest.Mocked>; + let globalConfig$: Observable; + + beforeEach(() => { + mockDataStart = dataPluginMock.createStartContract(); + mockCoreSetup = coreMock.createSetup({ pluginStartContract: mockDataStart }); + getStartServices = mockCoreSetup.getStartServices; + globalConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; + }); + + it('handler calls /_msearch with the given request', async () => { + const response = { id: 'yay' }; + const mockClient = { transport: { request: jest.fn().mockResolvedValue(response) } }; + const mockContext = { + core: { + elasticsearch: { client: { asCurrentUser: mockClient } }, + uiSettings: { client: { get: jest.fn() } }, + }, + }; + const mockBody = { searches: [{ header: {}, body: {} }] }; + const mockQuery = {}; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerMsearchRoute(mockCoreSetup.http.createRouter(), { getStartServices, globalConfig$ }); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient.transport.request.mock.calls[0][0].method).toBe('GET'); + expect(mockClient.transport.request.mock.calls[0][0].path).toBe('/_msearch'); + expect(mockClient.transport.request.mock.calls[0][0].body).toEqual( + convertRequestBody(mockBody as any, { timeout: '0ms' }) + ); + expect(mockResponse.ok).toBeCalled(); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ + body: response, + }); + }); + + it('handler throws an error if the search throws an error', async () => { + const response = { + message: 'oh no', + body: { + error: 'oops', + }, + }; + const mockClient = { + transport: { request: jest.fn().mockReturnValue(Promise.reject(response)) }, + }; + const mockContext = { + core: { + elasticsearch: { client: { asCurrentUser: mockClient } }, + uiSettings: { client: { get: jest.fn() } }, + }, + }; + const mockBody = { searches: [{ header: {}, body: {} }] }; + const mockQuery = {}; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerMsearchRoute(mockCoreSetup.http.createRouter(), { getStartServices, globalConfig$ }); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient.transport.request).toBeCalled(); + expect(mockResponse.customError).toBeCalled(); + + const error: any = mockResponse.customError.mock.calls[0][0]; + expect(error.body.message).toBe('oh no'); + expect(error.body.attributes.error).toBe('oops'); + }); + + describe('convertRequestBody', () => { + it('combines header & body into proper msearch request', () => { + const request = { + searches: [{ header: { index: 'foo', preference: 0 }, body: { test: true } }], + }; + expect(convertRequestBody(request, { timeout: '30000ms' })).toMatchInlineSnapshot(` + "{\\"ignore_unavailable\\":true,\\"index\\":\\"foo\\",\\"preference\\":0} + {\\"timeout\\":\\"30000ms\\",\\"test\\":true} + " + `); + }); + + it('handles multiple searches', () => { + const request = { + searches: [ + { header: { index: 'foo', preference: 0 }, body: { test: true } }, + { header: { index: 'bar', preference: 1 }, body: { hello: 'world' } }, + ], + }; + expect(convertRequestBody(request, { timeout: '30000ms' })).toMatchInlineSnapshot(` + "{\\"ignore_unavailable\\":true,\\"index\\":\\"foo\\",\\"preference\\":0} + {\\"timeout\\":\\"30000ms\\",\\"test\\":true} + {\\"ignore_unavailable\\":true,\\"index\\":\\"bar\\",\\"preference\\":1} + {\\"timeout\\":\\"30000ms\\",\\"hello\\":\\"world\\"} + " + `); + }); + }); +}); diff --git a/src/plugins/data/server/search/routes/msearch.ts b/src/plugins/data/server/search/routes/msearch.ts new file mode 100644 index 0000000000000..efb40edd90d58 --- /dev/null +++ b/src/plugins/data/server/search/routes/msearch.ts @@ -0,0 +1,136 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { first } from 'rxjs/operators'; +import { schema } from '@kbn/config-schema'; + +import { IRouter } from 'src/core/server'; +import { UI_SETTINGS } from '../../../common'; +import { SearchRouteDependencies } from '../search_service'; +import { getDefaultSearchParams } from '..'; + +interface MsearchHeaders { + index: string; + preference?: number | string; +} + +interface MsearchRequest { + header: MsearchHeaders; + body: any; +} + +interface RequestBody { + searches: MsearchRequest[]; +} + +/** @internal */ +export function convertRequestBody( + requestBody: RequestBody, + { timeout }: { timeout?: string } +): string { + return requestBody.searches.reduce((req, curr) => { + const header = JSON.stringify({ + ignore_unavailable: true, + ...curr.header, + }); + const body = JSON.stringify({ + timeout, + ...curr.body, + }); + return `${req}${header}\n${body}\n`; + }, ''); +} + +/** + * The msearch route takes in an array of searches, each consisting of header + * and body json, and reformts them into a single request for the _msearch API. + * + * The reason for taking requests in a different format is so that we can + * inject values into each request without needing to manually parse each one. + * + * This route is internal and _should not be used_ in any new areas of code. + * It only exists as a means of removing remaining dependencies on the + * legacy ES client. + * + * @deprecated + */ +export function registerMsearchRoute(router: IRouter, deps: SearchRouteDependencies): void { + router.post( + { + path: '/internal/_msearch', + validate: { + body: schema.object({ + searches: schema.arrayOf( + schema.object({ + header: schema.object( + { + index: schema.string(), + preference: schema.maybe(schema.oneOf([schema.number(), schema.string()])), + }, + { unknowns: 'allow' } + ), + body: schema.object({}, { unknowns: 'allow' }), + }) + ), + }), + }, + }, + async (context, request, res) => { + const client = context.core.elasticsearch.client.asCurrentUser; + + // get shardTimeout + const config = await deps.globalConfig$.pipe(first()).toPromise(); + const { timeout } = getDefaultSearchParams(config); + + const body = convertRequestBody(request.body, { timeout }); + + try { + const ignoreThrottled = !(await context.core.uiSettings.client.get( + UI_SETTINGS.SEARCH_INCLUDE_FROZEN + )); + const maxConcurrentShardRequests = await context.core.uiSettings.client.get( + UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS + ); + const response = await client.transport.request({ + method: 'GET', + path: '/_msearch', + body, + querystring: { + rest_total_hits_as_int: true, + ignore_throttled: ignoreThrottled, + max_concurrent_shard_requests: + maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined, + }, + }); + + return res.ok({ body: response }); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); +} diff --git a/src/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes/search.test.ts similarity index 78% rename from src/plugins/data/server/search/routes.test.ts rename to src/plugins/data/server/search/routes/search.test.ts index 1c5321885ec81..d4404c318ab47 100644 --- a/src/plugins/data/server/search/routes.test.ts +++ b/src/plugins/data/server/search/routes/search.test.ts @@ -17,19 +17,34 @@ * under the License. */ -import { CoreSetup, RequestHandlerContext } from '../../../../../src/core/server'; -import { coreMock, httpServerMock } from '../../../../../src/core/server/mocks'; -import { registerSearchRoute } from './routes'; -import { DataPluginStart } from '../plugin'; -import { dataPluginMock } from '../mocks'; +import { Observable } from 'rxjs'; + +import { + CoreSetup, + RequestHandlerContext, + SharedGlobalConfig, + StartServicesAccessor, +} from 'src/core/server'; +import { + coreMock, + httpServerMock, + pluginInitializerContextConfigMock, +} from '../../../../../../src/core/server/mocks'; +import { registerSearchRoute } from './search'; +import { DataPluginStart } from '../../plugin'; +import { dataPluginMock } from '../../mocks'; describe('Search service', () => { let mockDataStart: MockedKeys; - let mockCoreSetup: MockedKeys>; + let mockCoreSetup: MockedKeys>; + let getStartServices: jest.Mocked>; + let globalConfig$: Observable; beforeEach(() => { mockDataStart = dataPluginMock.createStartContract(); mockCoreSetup = coreMock.createSetup({ pluginStartContract: mockDataStart }); + getStartServices = mockCoreSetup.getStartServices; + globalConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; }); it('handler calls context.search.search with the given request and strategy', async () => { @@ -61,7 +76,7 @@ describe('Search service', () => { }); const mockResponse = httpServerMock.createResponseFactory(); - registerSearchRoute(mockCoreSetup); + registerSearchRoute(mockCoreSetup.http.createRouter(), { getStartServices, globalConfig$ }); const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; const handler = mockRouter.post.mock.calls[0][1]; @@ -92,7 +107,7 @@ describe('Search service', () => { }); const mockResponse = httpServerMock.createResponseFactory(); - registerSearchRoute(mockCoreSetup); + registerSearchRoute(mockCoreSetup.http.createRouter(), { getStartServices, globalConfig$ }); const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; const handler = mockRouter.post.mock.calls[0][1]; diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes/search.ts similarity index 84% rename from src/plugins/data/server/search/routes.ts rename to src/plugins/data/server/search/routes/search.ts index 3d813f745305f..4340285583489 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes/search.ts @@ -18,13 +18,14 @@ */ import { schema } from '@kbn/config-schema'; -import { CoreSetup } from '../../../../core/server'; -import { getRequestAbortedSignal } from '../lib'; -import { DataPluginStart } from '../plugin'; - -export function registerSearchRoute(core: CoreSetup): void { - const router = core.http.createRouter(); +import { IRouter } from 'src/core/server'; +import { getRequestAbortedSignal } from '../../lib'; +import { SearchRouteDependencies } from '../search_service'; +export function registerSearchRoute( + router: IRouter, + { getStartServices }: SearchRouteDependencies +): void { router.post( { path: '/internal/search/{strategy}/{id?}', @@ -42,16 +43,16 @@ export function registerSearchRoute(core: CoreSetup): v async (context, request, res) => { const searchRequest = request.body; const { strategy, id } = request.params; - const signal = getRequestAbortedSignal(request.events.aborted$); + const abortSignal = getRequestAbortedSignal(request.events.aborted$); - const [, , selfStart] = await core.getStartServices(); + const [, , selfStart] = await getStartServices(); try { const response = await selfStart.search.search( context, { ...searchRequest, id }, { - signal, + abortSignal, strategy, } ); @@ -85,7 +86,7 @@ export function registerSearchRoute(core: CoreSetup): v async (context, request, res) => { const { strategy, id } = request.params; - const [, , selfStart] = await core.getStartServices(); + const [, , selfStart] = await getStartServices(); const searchStrategy = selfStart.search.getSearchStrategy(strategy); if (!searchStrategy.cancel) return res.ok(); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index edc94961c79d8..e19d3dd8a5451 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Observable } from 'rxjs'; import { CoreSetup, CoreStart, @@ -24,20 +25,22 @@ import { Plugin, PluginInitializerContext, RequestHandlerContext, -} from '../../../../core/server'; + SharedGlobalConfig, + StartServicesAccessor, +} from 'src/core/server'; import { ISearchSetup, ISearchStart, ISearchStrategy, SearchEnhancements } from './types'; import { AggsService, AggsSetupDependencies } from './aggs'; import { FieldFormatsStart } from '../field_formats'; -import { registerSearchRoute } from './routes'; +import { registerMsearchRoute, registerSearchRoute } from './routes'; import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './es_search'; import { DataPluginStart } from '../plugin'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; import { searchTelemetry } from '../saved_objects'; -import { IEsSearchRequest, IEsSearchResponse } from '../../common'; +import { IEsSearchRequest, IEsSearchResponse, ISearchOptions } from '../../common'; type StrategyMap< SearchStrategyRequest extends IEsSearchRequest = IEsSearchRequest, @@ -55,6 +58,12 @@ export interface SearchServiceStartDependencies { fieldFormats: FieldFormatsStart; } +/** @internal */ +export interface SearchRouteDependencies { + getStartServices: StartServicesAccessor<{}, DataPluginStart>; + globalConfig$: Observable; +} + export class SearchService implements Plugin { private readonly aggsService = new AggsService(); private defaultSearchStrategyName: string = ES_SEARCH_STRATEGY; @@ -66,11 +75,19 @@ export class SearchService implements Plugin { ) {} public setup( - core: CoreSetup, + core: CoreSetup<{}, DataPluginStart>, { registerFunction, usageCollection }: SearchServiceSetupDependencies ): ISearchSetup { const usage = usageCollection ? usageProvider(core) : undefined; + const router = core.http.createRouter(); + const routeDependencies = { + getStartServices: core.getStartServices, + globalConfig$: this.initializerContext.config.legacy.globalConfig$, + }; + registerSearchRoute(router, routeDependencies); + registerMsearchRoute(router, routeDependencies); + this.registerSearchStrategy( ES_SEARCH_STRATEGY, esSearchStrategyProvider( @@ -85,8 +102,6 @@ export class SearchService implements Plugin { registerUsageCollector(usageCollection, this.initializerContext); } - registerSearchRoute(core); - return { __enhance: (enhancements: SearchEnhancements) => { if (this.searchStrategies.hasOwnProperty(enhancements.defaultStrategy)) { @@ -102,11 +117,13 @@ export class SearchService implements Plugin { private search( context: RequestHandlerContext, searchRequest: IEsSearchRequest, - options: Record + options: ISearchOptions ) { - return this.getSearchStrategy( - options.strategy || this.defaultSearchStrategyName - ).search(context, searchRequest, { signal: options.signal }); + return this.getSearchStrategy(options.strategy || this.defaultSearchStrategyName).search( + context, + searchRequest, + options + ); } public start( diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 77d7341b3ae71..b2b958454de48 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -18,6 +18,7 @@ */ import { RequestHandlerContext } from '../../../../core/server'; +import { ISearchOptions } from '../../common/search'; import { AggsSetup, AggsStart } from './aggs'; import { SearchUsage } from './collectors/usage'; import { IEsSearchRequest, IEsSearchResponse } from './es_search'; @@ -26,14 +27,6 @@ export interface SearchEnhancements { defaultStrategy: string; } -export interface ISearchOptions { - /** - * An `AbortSignal` that allows the caller of `search` to abort a search request. - */ - signal?: AbortSignal; - strategy?: string; -} - export interface ISearchSetup { aggs: AggsSetup; /** diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index ad0e905c33d27..a4f5f590e1774 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -48,7 +48,6 @@ import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; -import { FetchOptions } from 'src/plugins/data/public'; import { FieldStatsParams } from 'elasticsearch'; import { GenericParams } from 'elasticsearch'; import { GetParams } from 'elasticsearch'; @@ -99,6 +98,7 @@ import { IngestDeletePipelineParams } from 'elasticsearch'; import { IngestGetPipelineParams } from 'elasticsearch'; import { IngestPutPipelineParams } from 'elasticsearch'; import { IngestSimulateParams } from 'elasticsearch'; +import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { KibanaConfigType as KibanaConfigType_2 } from 'src/core/server/kibana_config'; @@ -570,7 +570,9 @@ export interface IFieldType { // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts // // (undocumented) - toSpec?: () => FieldSpec; + toSpec?: (options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }) => FieldSpec; // (undocumented) type: string; // (undocumented) @@ -681,8 +683,7 @@ export class IndexPatternsFetcher { // // @public (undocumented) export interface ISearchOptions { - signal?: AbortSignal; - // (undocumented) + abortSignal?: AbortSignal; strategy?: string; } @@ -1068,6 +1069,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // Warnings were encountered during analysis: // +// src/plugins/data/common/index_patterns/fields/types.ts:41:25 - (ae-forgotten-export) The symbol "IndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/public/application/angular/discover.html b/src/plugins/discover/public/application/angular/discover.html index d3d4f524873d8..94f13e1cd8132 100644 --- a/src/plugins/discover/public/application/angular/discover.html +++ b/src/plugins/discover/public/application/angular/discover.html @@ -31,7 +31,6 @@

{{screenTitle}}

on-remove-field="removeColumn" selected-index-pattern="searchSource.getField('index')" set-index-pattern="setIndexPattern" - state="state" > @@ -78,11 +77,11 @@

{{screenTitle}}

class="dscTimechart" ng-if="opts.timefield" > - {} + 'bytes' ); const props = { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index 639dbfe09277c..bb330cba68e2e 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -133,6 +133,9 @@ export function DiscoverField({ iconType="plusInCircleFilled" className="dscSidebarItem__action" onClick={(ev: React.MouseEvent) => { + if (ev.type === 'click') { + ev.currentTarget.focus(); + } ev.preventDefault(); ev.stopPropagation(); toggleDisplay(field); @@ -155,6 +158,9 @@ export function DiscoverField({ iconType="cross" className="dscSidebarItem__action" onClick={(ev: React.MouseEvent) => { + if (ev.type === 'click') { + ev.currentTarget.focus(); + } ev.preventDefault(); ev.stopPropagation(); toggleDisplay(field); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 1f27766a1756d..850624888b24a 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -30,7 +30,6 @@ import { SavedObject } from '../../../../../../core/types'; import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; import { IndexPatternField, IndexPattern, UI_SETTINGS } from '../../../../../data/public'; -import { AppState } from '../../angular/discover_state'; import { getDetails } from './lib/get_details'; import { getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter'; import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list'; @@ -74,10 +73,6 @@ export interface DiscoverSidebarProps { * Callback function to select another index pattern */ setIndexPattern: (id: string) => void; - /** - * Current app state, used for generating a link to visualize - */ - state: AppState; } export function DiscoverSidebar({ @@ -90,7 +85,6 @@ export function DiscoverSidebar({ onRemoveField, selectedIndexPattern, setIndexPattern, - state, }: DiscoverSidebarProps) { const [showFields, setShowFields] = useState(false); const [fields, setFields] = useState(null); @@ -111,8 +105,8 @@ export function DiscoverSidebar({ ); const getDetailsByField = useCallback( - (ipField: IndexPatternField) => getDetails(ipField, hits, columns), - [hits, columns] + (ipField: IndexPatternField) => getDetails(ipField, hits, columns, selectedIndexPattern), + [hits, columns, selectedIndexPattern] ); const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING); @@ -185,10 +179,10 @@ export function DiscoverSidebar({ aria-labelledby="selected_fields" data-test-subj={`fieldList-selected`} > - {selectedFields.map((field: IndexPatternField, idx: number) => { + {selectedFields.map((field: IndexPatternField) => { return (
  • @@ -260,10 +254,10 @@ export function DiscoverSidebar({ aria-labelledby="available_fields available_fields_popular" data-test-subj={`fieldList-popular`} > - {popularFields.map((field: IndexPatternField, idx: number) => { + {popularFields.map((field: IndexPatternField) => { return (
  • @@ -290,9 +284,13 @@ export function DiscoverSidebar({ aria-labelledby="available_fields" data-test-subj={`fieldList-unpopular`} > - {unpopularFields.map((field: IndexPatternField, idx: number) => { + {unpopularFields.map((field: IndexPatternField) => { return ( -
  • +
  • >, - columns: string[] + columns: string[], + indexPattern: IndexPattern ) { const details = { ...fieldCalculator.getFieldValueCounts({ hits, field, + indexPattern, count: 5, grouped: false, }), @@ -37,7 +39,7 @@ export function getDetails( }; if (details.buckets) { for (const bucket of details.buckets) { - bucket.display = field.format.convert(bucket.value); + bucket.display = indexPattern.getFormatterForField(field).convert(bucket.value); } } return details; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts b/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts index 00e00aa8e2991..c96a8f5ce17b9 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts @@ -31,6 +31,7 @@ export function getIndexPatternFieldList( difference(fieldNamesInDocs, fieldNamesInIndexPattern).forEach((unknownFieldName) => { unknownTypes.push({ + displayName: String(unknownFieldName), name: String(unknownFieldName), type: 'unknown', } as IndexPatternField); diff --git a/src/plugins/discover/public/saved_searches/saved_searches.ts b/src/plugins/discover/public/saved_searches/saved_searches.ts index 09be10b137494..0bc332ed8ec74 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches.ts @@ -22,11 +22,7 @@ import { createSavedSearchClass } from './_saved_search'; export function createSavedSearchesLoader(services: SavedObjectKibanaServices) { const SavedSearchClass = createSavedSearchClass(services); - const savedSearchLoader = new SavedObjectLoader( - SavedSearchClass, - services.savedObjectsClient, - services.chrome - ); + const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, services.savedObjectsClient); // Customize loader properties since adding an 's' on type doesn't work for type 'search' . savedSearchLoader.loaderProperties = { name: 'searches', diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts index 780cff9f4be7e..184178ba80e84 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts @@ -18,12 +18,7 @@ */ import { cloneDeep } from 'lodash'; -import { - ScopedHistory, - ApplicationStart, - PublicLegacyAppInfo, - PublicAppInfo, -} from '../../../../../core/public'; +import { ScopedHistory, ApplicationStart, PublicAppInfo } from '../../../../../core/public'; import { EmbeddableEditorState, isEmbeddableEditorState, @@ -41,7 +36,7 @@ export class EmbeddableStateTransfer { constructor( private navigateToApp: ApplicationStart['navigateToApp'], private scopedHistory?: ScopedHistory, - private appList?: ReadonlyMap | undefined + private appList?: ReadonlyMap | undefined ) {} /** diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index fb09729ab71c3..2ca31994b722d 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -29,7 +29,6 @@ import { Plugin, ScopedHistory, PublicAppInfo, - PublicLegacyAppInfo, } from '../../../core/public'; import { EmbeddableFactoryRegistry, EmbeddableFactoryProvider } from './types'; import { bootstrap } from './bootstrap'; @@ -92,7 +91,7 @@ export class EmbeddablePublicPlugin implements Plugin; + private appList?: ReadonlyMap; private appListSubscription?: Subscription; constructor(initializerContext: PluginInitializerContext) {} diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx index 548e477c7c411..4dd9cfcaff16b 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx @@ -160,9 +160,7 @@ export const useGlobalFlyout = () => { Array.from(getContents()).forEach(removeContent); } }; - // https://github.com/elastic/kibana/issues/73970 - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [removeContent]); + }, [removeContent, getContents]); return { ...ctx, addContent }; }; diff --git a/src/plugins/es_ui_shared/public/components/json_editor/index.ts b/src/plugins/es_ui_shared/public/components/json_editor/index.ts index 81476a65f4215..63319baa38f5c 100644 --- a/src/plugins/es_ui_shared/public/components/json_editor/index.ts +++ b/src/plugins/es_ui_shared/public/components/json_editor/index.ts @@ -19,4 +19,4 @@ export * from './json_editor'; -export { OnJsonEditorUpdateHandler } from './use_json'; +export { OnJsonEditorUpdateHandler, JsonEditorState } from './use_json'; diff --git a/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx b/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx index 7d21722781d60..206db5a285620 100644 --- a/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx +++ b/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx @@ -17,98 +17,97 @@ * under the License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiFormRow, EuiCodeEditor } from '@elastic/eui'; import { debounce } from 'lodash'; -import { isJSON } from '../../../static/validators/string'; import { useJson, OnJsonEditorUpdateHandler } from './use_json'; -interface Props { - onUpdate: OnJsonEditorUpdateHandler; +interface Props { + onUpdate: OnJsonEditorUpdateHandler; label?: string; helpText?: React.ReactNode; value?: string; - defaultValue?: { [key: string]: any }; + defaultValue?: T; euiCodeEditorProps?: { [key: string]: any }; error?: string | null; } -export const JsonEditor = React.memo( - ({ - label, - helpText, +function JsonEditorComp({ + label, + helpText, + onUpdate, + value, + defaultValue, + euiCodeEditorProps, + error: propsError, +}: Props) { + const { content, setContent, error: internalError, isControlled } = useJson({ + defaultValue, onUpdate, value, - defaultValue, - euiCodeEditorProps, - error: propsError, - }: Props) => { - const isControlled = value !== undefined; + }); - const { content, setContent, error: internalError } = useJson({ - defaultValue, - onUpdate, - isControlled, - }); + const debouncedSetContent = useMemo(() => { + return debounce(setContent, 300); + }, [setContent]); - // https://github.com/elastic/kibana/issues/73971 - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - const debouncedSetContent = useCallback(debounce(setContent, 300), [setContent]); + // We let the consumer control the validation and the error message. + const error = isControlled ? propsError : internalError; - // We let the consumer control the validation and the error message. - const error = isControlled ? propsError : internalError; + const onEuiCodeEditorChange = useCallback( + (updated: string) => { + if (isControlled) { + onUpdate({ + data: { + raw: updated, + format: () => JSON.parse(updated), + }, + validate: () => { + try { + JSON.parse(updated); + return true; + } catch (e) { + return false; + } + }, + isValid: undefined, + }); + } else { + debouncedSetContent(updated); + } + }, + [isControlled, debouncedSetContent, onUpdate] + ); - const onEuiCodeEditorChange = useCallback( - (updated: string) => { - if (isControlled) { - onUpdate({ - data: { - raw: updated, - format() { - return JSON.parse(updated); - }, - }, - validate() { - return isJSON(updated); - }, - isValid: undefined, - }); - } else { - debouncedSetContent(updated); - } - }, - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - [isControlled] - ); + return ( + + + + ); +} - return ( - - - - ); - } -); +export const JsonEditor = React.memo(JsonEditorComp) as typeof JsonEditorComp; diff --git a/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts b/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts index 0ba39f5f05fe6..47d518e6814a4 100644 --- a/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts +++ b/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts @@ -17,24 +17,28 @@ * under the License. */ -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useState, useRef, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { isJSON } from '../../../static/validators/string'; -export type OnJsonEditorUpdateHandler = (arg: { +export interface JsonEditorState { data: { raw: string; format(): T; }; validate(): boolean; isValid: boolean | undefined; -}) => void; +} + +export type OnJsonEditorUpdateHandler = ( + arg: JsonEditorState +) => void; interface Parameters { onUpdate: OnJsonEditorUpdateHandler; defaultValue?: T; - isControlled?: boolean; + value?: string; } const stringifyJson = (json: { [key: string]: any }) => @@ -43,13 +47,16 @@ const stringifyJson = (json: { [key: string]: any }) => export const useJson = ({ defaultValue = {} as T, onUpdate, - isControlled = false, + value, }: Parameters) => { - const didMount = useRef(false); - const [content, setContent] = useState(stringifyJson(defaultValue)); + const isControlled = value !== undefined; + const isMounted = useRef(false); + const [content, setContent] = useState( + isControlled ? value! : stringifyJson(defaultValue) + ); const [error, setError] = useState(null); - const validate = () => { + const validate = useCallback(() => { // We allow empty string as it will be converted to "{}"" const isValid = content.trim() === '' ? true : isJSON(content); if (!isValid) { @@ -62,35 +69,43 @@ export const useJson = ({ setError(null); } return isValid; - }; + }, [content]); - const formatContent = () => { + const formatContent = useCallback(() => { const isValid = validate(); const data = isValid && content.trim() !== '' ? JSON.parse(content) : {}; return data as T; - }; + }, [validate, content]); useEffect(() => { - if (didMount.current) { - const isValid = isControlled ? undefined : validate(); - onUpdate({ - data: { - raw: content, - format: formatContent, - }, - validate, - isValid, - }); - } else { - didMount.current = true; + if (!isMounted.current || isControlled) { + return; } - // https://github.com/elastic/kibana/issues/73971 - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [content]); + + const isValid = validate(); + + onUpdate({ + data: { + raw: content, + format: formatContent, + }, + validate, + isValid, + }); + }, [onUpdate, content, formatContent, validate, isControlled]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); return { content, setContent, error, + isControlled, }; }; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index 995ae0ba42837..5a1c13658604a 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -26,7 +26,7 @@ import * as Monaco from './monaco'; import * as ace from './ace'; import * as GlobalFlyout from './global_flyout'; -export { JsonEditor, OnJsonEditorUpdateHandler } from './components/json_editor'; +export { JsonEditor, OnJsonEditorUpdateHandler, JsonEditorState } from './components/json_editor'; export { SectionLoading } from './components/section_loading'; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx index b83b0af5f97c6..9ffa7adace781 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx @@ -31,17 +31,16 @@ interface Props { export const RangeField = ({ field, euiFieldProps = {}, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const { onChange: onFieldChange } = field; const onChange = useCallback( (e: React.ChangeEvent | React.MouseEvent) => { const event = ({ ...e, value: `${e.currentTarget.value}` } as unknown) as React.ChangeEvent<{ value: string; }>; - field.onChange(event); + onFieldChange(event); }, - // https://github.com/elastic/kibana/issues/73972 - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - [field.onChange] + [onFieldChange] ); return ( diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx index a55b2f0a8fa29..c14471991ccd3 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.test.tsx @@ -64,6 +64,93 @@ describe('', () => { }); }); + describe('validation', () => { + let formHook: FormHook | null = null; + + beforeEach(() => { + formHook = null; + }); + + const onFormHook = (form: FormHook) => { + formHook = form; + }; + + const getTestComp = (fieldConfig: FieldConfig) => { + const TestComp = ({ onForm }: { onForm: (form: FormHook) => void }) => { + const { form } = useForm(); + + useEffect(() => { + onForm(form); + }, [onForm, form]); + + return ( +
    + + + ); + }; + return TestComp; + }; + + const setup = (fieldConfig: FieldConfig) => { + return registerTestBed(getTestComp(fieldConfig), { + memoryRouter: { wrapComponent: false }, + defaultProps: { onForm: onFormHook }, + })() as TestBed; + }; + + test('should update the form validity whenever the field value changes', async () => { + const fieldConfig: FieldConfig = { + defaultValue: '', // empty string, which is not valid + validations: [ + { + validator: ({ value }) => { + // Validate that string is not empty + if ((value as string).trim() === '') { + return { message: 'Error: field is empty.' }; + } + }, + }, + ], + }; + + // Mount our TestComponent + const { + form: { setInputValue }, + } = setup(fieldConfig); + + if (formHook === null) { + throw new Error('FormHook object has not been set.'); + } + + let { isValid } = formHook; + expect(isValid).toBeUndefined(); // Initially the form validity is undefined... + + await act(async () => { + await formHook!.validate(); // ...until we validate the form + }); + + ({ isValid } = formHook); + expect(isValid).toBe(false); + + // Change to a non empty string to pass validation + await act(async () => { + setInputValue('myField', 'changedValue'); + }); + + ({ isValid } = formHook); + expect(isValid).toBe(true); + + // Change back to an empty string to fail validation + await act(async () => { + setInputValue('myField', ''); + }); + + ({ isValid } = formHook); + expect(isValid).toBe(false); + }); + }); + describe('serializer(), deserializer(), formatter()', () => { interface MyForm { name: string; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index fa29f900af2ef..f01c7226ea4ce 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -69,11 +69,12 @@ export const useField = ( const [isChangingValue, setIsChangingValue] = useState(false); const [isValidated, setIsValidated] = useState(false); + const isMounted = useRef(false); const validateCounter = useRef(0); const changeCounter = useRef(0); + const hasBeenReset = useRef(false); const inflightValidation = useRef | null>(null); const debounceTimeout = useRef(null); - const isMounted = useRef(false); // -- HELPERS // ---------------------------------- @@ -142,11 +143,7 @@ export const useField = ( __updateFormDataAt(path, value); // Validate field(s) (that will update form.isValid state) - // We only validate if the value is different than the initial or default value - // to avoid validating after a form.reset() call. - if (value !== initialValue && value !== defaultValue) { - await __validateFields(fieldsToValidateOnChange ?? [path]); - } + await __validateFields(fieldsToValidateOnChange ?? [path]); if (isMounted.current === false) { return; @@ -172,8 +169,6 @@ export const useField = ( }, [ path, value, - defaultValue, - initialValue, valueChangeListener, errorDisplayDelay, fieldsToValidateOnChange, @@ -468,6 +463,7 @@ export const useField = ( setErrors([]); if (resetValue) { + hasBeenReset.current = true; const newValue = deserializeValue(updatedDefaultValue ?? defaultValue); setValue(newValue); return newValue; @@ -539,6 +535,13 @@ export const useField = ( }, [path, __removeField]); useEffect(() => { + // If the field value has been reset, we don't want to call the "onValueChange()" + // as it will set the "isPristine" state to true or validate the field, which initially we don't want. + if (hasBeenReset.current) { + hasBeenReset.current = false; + return; + } + if (!isMounted.current) { return; } diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index 4a880415b6d22..edcd84daf5d2f 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -22,7 +22,13 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, getRandomString, TestBed } from '../shared_imports'; import { Form, UseField } from '../components'; -import { FormSubmitHandler, OnUpdateHandler, FormHook, ValidationFunc } from '../types'; +import { + FormSubmitHandler, + OnUpdateHandler, + FormHook, + ValidationFunc, + FieldConfig, +} from '../types'; import { useForm } from './use_form'; interface MyForm { @@ -441,5 +447,57 @@ describe('useForm() hook', () => { deeply: { nested: { value: '' } }, // Fallback to empty string as no config was provided }); }); + + test('should not validate the fields after resetting its value (form validity should be undefined)', async () => { + const fieldConfig: FieldConfig = { + defaultValue: '', + validations: [ + { + validator: ({ value }) => { + if ((value as string).trim() === '') { + return { message: 'Error: empty string' }; + } + }, + }, + ], + }; + + const TestResetComp = () => { + const { form } = useForm(); + + useEffect(() => { + formHook = form; + }, [form]); + + return ( +
    + + + ); + }; + + const { + form: { setInputValue }, + } = registerTestBed(TestResetComp, { + memoryRouter: { wrapComponent: false }, + })() as TestBed; + + let { isValid } = formHook!; + expect(isValid).toBeUndefined(); + + await act(async () => { + setInputValue('myField', 'changedValue'); + }); + ({ isValid } = formHook!); + expect(isValid).toBe(true); + + await act(async () => { + // When we reset the form, value is back to "", which is invalid for the field + formHook!.reset(); + }); + + ({ isValid } = formHook!); + expect(isValid).toBeUndefined(); // Make sure it is "undefined" and not "false". + }); }); }); diff --git a/src/plugins/home/public/application/application.tsx b/src/plugins/home/public/application/application.tsx index 5d71bf8651d88..a4a158bc7dbde 100644 --- a/src/plugins/home/public/application/application.tsx +++ b/src/plugins/home/public/application/application.tsx @@ -47,6 +47,13 @@ export const renderApp = async ( chrome.setBreadcrumbs([{ text: homeTitle }]); + // dispatch synthetic hash change event to update hash history objects + // this is necessary because hash updates triggered by using popState won't trigger this event naturally. + // This must be called before the app is mounted to avoid call this after the redirect to default app logic kicks in + const unlisten = history.listen((location) => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); + render( @@ -54,12 +61,6 @@ export const renderApp = async ( element ); - // dispatch synthetic hash change event to update hash history objects - // this is necessary because hash updates triggered by using popState won't trigger this event naturally. - const unlisten = history.listen(() => { - window.dispatchEvent(new HashChangeEvent('hashchange')); - }); - return () => { unmountComponentAtNode(element); unlisten(); diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts index 4c31ccee1243a..37657912deb95 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts @@ -248,11 +248,11 @@ export const getSavedObjects = (): SavedObject[] => [ version: '1', migrationVersion: {}, attributes: { - title: i18n.translate('home.sampleData.ecommerceSpec.averageSalesPerRegionTitle', { - defaultMessage: '[eCommerce] Average Sales Per Region', + title: i18n.translate('home.sampleData.ecommerceSpec.salesCountMapTitle', { + defaultMessage: '[eCommerce] Sales Count Map', }), visState: - '{"title":"[eCommerce] Average Sales Per Region","type":"region_map","params":{"legendPosition":"bottomright","addTooltip":true,"colorSchema":"Blues","selectedLayer":{"attribution":"

    Made with NaturalEarth|Elastic Maps Service

    ","weight":1,"name":"World Countries","url":"https://vector.maps.elastic.co/blob/5659313586569216?elastic_tile_service_tos=agree&my_app_version=7.0.0-alpha1&license=f6c534b8-91b9-4499-8804-a2e9789ecc95","format":{"type":"geojson"},"fields":[{"name":"iso2","description":"Two letter abbreviation"},{"name":"name","description":"Country name"},{"name":"iso3","description":"Three letter abbreviation"}],"created_at":"2017-04-26T17:12:15.978370","tags":[],"id":5659313586569216,"layerId":"elastic_maps_service.World Countries","isEMS":true},"emsHotLink":"https://maps.elastic.co/v2#file/World Countries","selectedJoinField":{"name":"iso2","description":"Two letter abbreviation"},"isDisplayWarning":true,"wms":{"enabled":false,"options":{"format":"image/png","transparent":true}},"mapZoom":2,"mapCenter":[0,0],"outlineWeight":1,"showAllShapes":true},"aggs":[{"id":"1","enabled":true,"type":"avg","schema":"metric","params":{"field":"taxful_total_price","customLabel":"Average Sale"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"geoip.country_iso_code","size":100,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[eCommerce] Sales Count Map","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 25, longitude: -40, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_ecommerce\\n %context%: true\\n %timefield%: order_date\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\"geoip.location\\", precision: 4, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\"geoip.location\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.gridSplit.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n size: {scale: \\"gridSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n }\\n }\\n },\\n {\\n name: gridLabel\\n type: text\\n from: {data: \\"table\\"}\\n encode: {\\n enter: {\\n fill: {value: \\"firebrick\\"}\\n text: {signal: \\"datum.doc_count\\"}\\n }\\n update: {\\n x: {signal: \\"datum.x\\"}\\n y: {signal: \\"datum.y\\"}\\n dx: {value: -6}\\n dy: {value: 6}\\n fontSize: {value: 18}\\n fontWeight: {value: \\"bold\\"}\\n }\\n }\\n }\\n ]\\n}"}}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts index c1f7fcb75b149..6f701d75e7d52 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts @@ -281,11 +281,10 @@ export const getSavedObjects = (): SavedObject[] => [ version: '1', migrationVersion: {}, attributes: { - title: i18n.translate('home.sampleData.flightsSpec.originCountryTicketPricesTitle', { - defaultMessage: '[Flights] Origin Country Ticket Prices', + title: i18n.translate('home.sampleData.flightsSpec.departuresCountMapTitle', { + defaultMessage: '[Flights] Departures Count Map', }), - visState: - '{"title":"[Flights] Origin Country Ticket Prices","type":"region_map","params":{"legendPosition":"bottomright","addTooltip":true,"colorSchema":"Blues","selectedLayer":{"attribution":"

    Made with NaturalEarth | Elastic Maps Service

    ","name":"World Countries","weight":1,"format":{"type":"geojson"},"url":"https://vector.maps.elastic.co/blob/5659313586569216?elastic_tile_service_tos=agree&my_app_version=6.3.0&license=686f9ec6-d775-44f0-b334-38caf85da617","fields":[{"name":"iso2","description":"Two letter abbreviation"},{"name":"name","description":"Country name"},{"name":"iso3","description":"Three letter abbreviation"}],"created_at":"2017-04-26T17:12:15.978370","tags":[],"id":5659313586569216,"layerId":"elastic_maps_service.World Countries"},"selectedJoinField":{"name":"iso2","description":"Two letter abbreviation"},"isDisplayWarning":false,"wms":{"enabled":false,"options":{"format":"image/png","transparent":true},"baseLayersAreLoaded":{},"tmsLayers":[{"id":"road_map","url":"https://tiles.maps.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0&license=686f9ec6-d775-44f0-b334-38caf85da617","minZoom":0,"maxZoom":18,"attribution":"

    © OpenStreetMap contributors | Elastic Maps Service

    ","subdomains":[]}],"selectedTmsLayer":{"id":"road_map","url":"https://tiles.maps.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0&license=686f9ec6-d775-44f0-b334-38caf85da617","minZoom":0,"maxZoom":18,"attribution":"

    © OpenStreetMap contributors | Elastic Maps Service

    ","subdomains":[]}},"mapZoom":2,"mapCenter":[0,0],"outlineWeight":1,"showAllShapes":true},"aggs":[{"id":"1","enabled":true,"type":"avg","schema":"metric","params":{"field":"AvgTicketPrice"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"OriginCountry","size":100,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: '{\"title\":\"[Flights] Departure Count Map\",\"type\":\"vega\",\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -40, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\\"OriginLocation\\\", precision: 4, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\\"OriginLocation\\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.gridSplit.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"gridSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"}}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts index 97258c21bc8f0..f8d39e6689fa8 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts @@ -51,11 +51,11 @@ export const getSavedObjects = (): SavedObject[] => [ version: '1', migrationVersion: {}, attributes: { - title: i18n.translate('home.sampleData.logsSpec.uniqueVisitorsByCountryTitle', { - defaultMessage: '[Logs] Unique Visitors by Country', + title: i18n.translate('home.sampleData.logsSpec.visitorsMapTitle', { + defaultMessage: '[Logs] Visitors Map', }), visState: - '{"title":"[Logs] Unique Visitors by Country","type":"region_map","params":{"legendPosition":"bottomright","addTooltip":true,"colorSchema":"Reds","selectedLayer":{"attribution":"

    Made with NaturalEarth | Elastic Maps Service

    ","name":"World Countries","weight":1,"format":{"type":"geojson"},"url":"https://vector.maps.elastic.co/blob/5659313586569216?elastic_tile_service_tos=agree&my_app_version=6.2.3&license=77ab0ecf-a521-499d-bd52-fbd740bb81d0","fields":[{"name":"iso2","description":"Two letter abbreviation"},{"name":"name","description":"Country name"},{"name":"iso3","description":"Three letter abbreviation"}],"created_at":"2017-04-26T17:12:15.978370","tags":[],"id":5659313586569216,"layerId":"elastic_maps_service.World Countries"},"selectedJoinField":{"name":"iso2","description":"Two letter abbreviation"},"isDisplayWarning":false,"wms":{"enabled":false,"options":{"format":"image/png","transparent":true},"baseLayersAreLoaded":{},"tmsLayers":[{"id":"road_map","url":"https://tiles.maps.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.2.3&license=77ab0ecf-a521-499d-bd52-fbd740bb81d0","minZoom":0,"maxZoom":18,"attribution":"

    © OpenStreetMap contributors | Elastic Maps Service

    ","subdomains":[]}],"selectedTmsLayer":{"id":"road_map","url":"https://tiles.maps.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.2.3&license=77ab0ecf-a521-499d-bd52-fbd740bb81d0","minZoom":0,"maxZoom":18,"attribution":"

    © OpenStreetMap contributors | Elastic Maps Service

    ","subdomains":[]}},"mapZoom":2,"mapCenter":[0,0],"outlineWeight":1,"showAllShapes":true,"emsHotLink":null},"aggs":[{"id":"1","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"clientip","customLabel":"Unique Visitors"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"geo.src","size":50,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[Logs] Visitors Map","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 30, longitude: -120, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_logs\\n %context%: true\\n %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\"geo.coordinates\\", precision: 5, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\"geo.coordinates\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.gridSplit.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n {\\n name: bubbleColor\\n type: linear\\n domain: {\\n data: table\\n field: doc_count\\n }\\n range: [\\"rgb(249, 234, 197)\\",\\"rgb(243, 200, 154)\\",\\"rgb(235, 166, 114)\\", \\"rgb(231, 102, 76)\\"]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n fill: {\\n scale: bubbleColor\\n field: doc_count\\n }\\n size: {scale: \\"gridSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n tooltip: {\\n signal: \\"{flights: datum.doc_count}\\"\\n }\\n }\\n }\\n }\\n ]\\n}"}}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap index 47cabc4df662f..45253f6ad27c0 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap @@ -15,13 +15,10 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = ` "displayName": "Elastic", "excluded": false, "format": undefined, - "indexPattern": Object { - "getNonScriptedFields": [Function], - }, "info": Array [], "name": "Elastic", "searchable": true, - "type": "name", + "type": "string", }, ] } @@ -44,9 +41,6 @@ exports[`IndexedFieldsTable should filter based on the type filter 1`] = ` "displayName": "timestamp", "excluded": false, "format": undefined, - "indexPattern": Object { - "getNonScriptedFields": [Function], - }, "info": Array [], "name": "timestamp", "type": "date", @@ -72,21 +66,15 @@ exports[`IndexedFieldsTable should render normally 1`] = ` "displayName": "Elastic", "excluded": false, "format": undefined, - "indexPattern": Object { - "getNonScriptedFields": [Function], - }, "info": Array [], "name": "Elastic", "searchable": true, - "type": "name", + "type": "string", }, Object { "displayName": "timestamp", "excluded": false, "format": undefined, - "indexPattern": Object { - "getNonScriptedFields": [Function], - }, "info": Array [], "name": "timestamp", "type": "date", @@ -95,9 +83,6 @@ exports[`IndexedFieldsTable should render normally 1`] = ` "displayName": "conflictingField", "excluded": false, "format": undefined, - "indexPattern": Object { - "getNonScriptedFields": [Function], - }, "info": Array [], "name": "conflictingField", "type": "conflict", diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 411bbe23e4761..319b9b2b3fce2 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { IndexPatternField, IIndexPattern, IndexPattern } from 'src/plugins/data/public'; +import { IndexPatternField, IIndexPattern } from 'src/plugins/data/public'; import { IndexedFieldsTable } from './indexed_fields_table'; jest.mock('@elastic/eui', () => ({ @@ -47,10 +47,8 @@ const indexPattern = ({ const mockFieldToIndexPatternField = (spec: Record) => { return new IndexPatternField( - indexPattern as IndexPattern, (spec as unknown) as IndexPatternField['spec'], - spec.displayName as string, - () => {} + spec.displayName as string ); }; @@ -59,7 +57,7 @@ const fields = [ name: 'Elastic', displayName: 'Elastic', searchable: true, - type: 'name', + type: 'string', }, { name: 'timestamp', displayName: 'timestamp', type: 'date' }, { name: 'conflictingField', displayName: 'conflictingField', type: 'conflict' }, diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 90f81a88b3da0..23977aac7fa7a 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -77,7 +77,6 @@ export class IndexedFieldsTable extends Component< return { ...field.spec, displayName: field.displayName, - indexPattern: field.indexPattern, format: getFieldFormat(indexPattern, field.name), excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field), diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx index 6c9d6db8de130..3bc9cd34f2984 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -176,7 +176,7 @@ export function Tabs({ indexPattern, fields, history, location }: TabsProps) { indexedFieldTypeFilter={indexedFieldTypeFilter} helpers={{ redirectToRoute: (field: IndexPatternField) => { - history.push(getPath(field)); + history.push(getPath(field, indexPattern)); }, getFieldInfo: indexPatternManagementStart.list.getFieldInfo, }} @@ -195,7 +195,7 @@ export function Tabs({ indexPattern, fields, history, location }: TabsProps) { scriptedFieldLanguageFilter={scriptedFieldLanguageFilter} helpers={{ redirectToRoute: (field: IndexPatternField) => { - history.push(getPath(field)); + history.push(getPath(field, indexPattern)); }, }} onRemoveField={refreshFilters} diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts index b422de93de7a9..91c5cc1afdb49 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts @@ -116,8 +116,8 @@ export function getTabs( return tabs; } -export function getPath(field: IndexPatternField) { - return `/patterns/${field.indexPattern?.id}/field/${field.name}`; +export function getPath(field: IndexPatternField, indexPattern: IndexPattern) { + return `/patterns/${indexPattern?.id}/field/${field.name}`; } const allTypesDropDown = i18n.translate( diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx index 96d3fc549ece0..b0385a61a72ac 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx @@ -138,12 +138,12 @@ describe('FieldEditor', () => { name: 'test', script: 'doc.test.value', }; - fieldList.push(testField as IndexPatternField); + fieldList.push((testField as unknown) as IndexPatternField); indexPattern.fields.getByName = (name) => { const flds = { [testField.name]: testField, }; - return flds[name] as IndexPatternField; + return (flds[name] as unknown) as IndexPatternField; }; const component = createComponentWithContext( @@ -173,7 +173,7 @@ describe('FieldEditor', () => { const flds = { [testField.name]: testField, }; - return flds[name] as IndexPatternField; + return (flds[name] as unknown) as IndexPatternField; }; const component = createComponentWithContext( diff --git a/src/plugins/input_control_vis/public/control/control.ts b/src/plugins/input_control_vis/public/control/control.ts index 91e8f1b26164b..da2dc7bab7cf7 100644 --- a/src/plugins/input_control_vis/public/control/control.ts +++ b/src/plugins/input_control_vis/public/control/control.ts @@ -81,9 +81,10 @@ export abstract class Control { abstract destroy(): void; format = (value: any) => { + const indexPattern = this.filterManager.getIndexPattern(); const field = this.filterManager.getField(); - if (field?.format?.convert) { - return field.format.convert(value); + if (field) { + return indexPattern.getFormatterForField(field).convert(value); } return value; diff --git a/src/plugins/kibana_legacy/public/angular/angular_config.tsx b/src/plugins/kibana_legacy/public/angular/angular_config.tsx index eafcbfda3db00..9dae615bc3848 100644 --- a/src/plugins/kibana_legacy/public/angular/angular_config.tsx +++ b/src/plugins/kibana_legacy/public/angular/angular_config.tsx @@ -27,12 +27,12 @@ import { } from 'angular'; import $ from 'jquery'; import { set } from '@elastic/safer-lodash-set'; -import { cloneDeep, forOwn, get } from 'lodash'; +import { get } from 'lodash'; import * as Rx from 'rxjs'; import { ChromeBreadcrumb, EnvironmentMode, PackageInfo } from 'kibana/public'; import { History } from 'history'; -import { CoreStart, LegacyCoreStart } from 'kibana/public'; +import { CoreStart } from 'kibana/public'; import { isSystemApiRequest } from '../utils'; import { formatAngularHttpError, isAngularHttpError } from '../notify/lib'; @@ -72,32 +72,18 @@ function isDummyRoute($route: any, isLocalAngular: boolean) { export const configureAppAngularModule = ( angularModule: IModule, - newPlatform: - | LegacyCoreStart - | { - core: CoreStart; - readonly env: { - mode: Readonly; - packageInfo: Readonly; - }; - }, + newPlatform: { + core: CoreStart; + readonly env: { + mode: Readonly; + packageInfo: Readonly; + }; + }, isLocalAngular: boolean, getHistory?: () => History ) => { const core = 'core' in newPlatform ? newPlatform.core : newPlatform; - const packageInfo = - 'env' in newPlatform - ? newPlatform.env.packageInfo - : newPlatform.injectedMetadata.getLegacyMetadata(); - - if ('injectedMetadata' in newPlatform) { - forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => { - if (name !== undefined) { - // The legacy platform modifies some of these values, clone to an unfrozen object. - angularModule.value(name, cloneDeep(val)); - } - }); - } + const packageInfo = newPlatform.env.packageInfo; angularModule .value('kbnVersion', packageInfo.version) @@ -105,13 +91,7 @@ export const configureAppAngularModule = ( .value('buildSha', packageInfo.buildSha) .value('esUrl', getEsUrl(core)) .value('uiCapabilities', core.application.capabilities) - .config( - setupCompileProvider( - 'injectedMetadata' in newPlatform - ? newPlatform.injectedMetadata.getLegacyMetadata().devMode - : newPlatform.env.mode.dev - ) - ) + .config(setupCompileProvider(newPlatform.env.mode.dev)) .config(setupLocationProvider()) .config($setupXsrfRequestInterceptor(packageInfo.version)) .run(capture$httpLoadingCount(core)) diff --git a/src/plugins/kibana_react/public/field_button/field_button.tsx b/src/plugins/kibana_react/public/field_button/field_button.tsx index e5833b261946a..26e6453e4c48b 100644 --- a/src/plugins/kibana_react/public/field_button/field_button.tsx +++ b/src/plugins/kibana_react/public/field_button/field_button.tsx @@ -100,7 +100,16 @@ export function FieldButton({ return (
    - + +
    + + + @@ -588,6 +665,7 @@ exports[`extend index management ilm summary extension should return extension w "step": "complete", "step_time_millis": 1544187775867, }, + "isFrozen": false, "name": "testy3", "primary": "1", "primary_size": "6.5kb", diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap similarity index 70% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap rename to x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap index ad3e0956fcf25..cbb9f82888701 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.js.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/__snapshots__/policy_table.test.tsx.snap @@ -52,68 +52,60 @@ exports[`policy table should show empty state when there are not any policies 1`
    -
    +
    + +

    + Create your first index lifecycle policy +

    + class="euiText euiText--medium" + > +

    + An index lifecycle policy helps you manage your indices as they age. +

    +
    + +
    +
    + -
    -
    + +
    diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx similarity index 68% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js rename to x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx index 60e3e9443bec9..d95b4503c266b 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx @@ -4,54 +4,62 @@ * you may not use this file except in compliance with the Elastic License. */ import moment from 'moment-timezone'; -import React from 'react'; -import { Provider } from 'react-redux'; -// axios has a $http like interface so using it to simulate $http -import axios from 'axios'; -import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import sinon from 'sinon'; +import React, { ReactElement } from 'react'; +import { ReactWrapper } from 'enzyme'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject, takeMountedSnapshot } from '@elastic/eui/lib/test'; -import { scopedHistoryMock } from '../../../../../src/core/public/mocks'; -import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; -import { fetchedPolicies } from '../../public/application/store/actions'; -import { indexLifecycleManagementStore } from '../../public/application/store'; -import { PolicyTable } from '../../public/application/sections/policy_table'; +import { + fatalErrorsServiceMock, + injectedMetadataServiceMock, + scopedHistoryMock, +} from '../../../../../src/core/public/mocks'; +import { HttpService } from '../../../../../src/core/public/http'; +import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; + +import { PolicyTable } from '../../public/application/sections/policy_table/policy_table'; import { init as initHttp } from '../../public/application/services/http'; import { init as initUiMetric } from '../../public/application/services/ui_metric'; +import { PolicyFromES } from '../../public/application/services/policies/types'; -initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path); -initUiMetric({ reportUiStats: () => {} }); - -let server = null; +initHttp( + new HttpService().setup({ + injectedMetadata: injectedMetadataServiceMock.createSetupContract(), + fatalErrors: fatalErrorsServiceMock.createSetupContract(), + }) +); +initUiMetric(usageCollectionPluginMock.createSetupContract()); -let store = null; -const policies = []; +const policies: PolicyFromES[] = []; for (let i = 0; i < 105; i++) { policies.push({ version: i, - modified_date: moment().subtract(i, 'days').valueOf(), - linkedIndices: i % 2 === 0 ? [`index${i}`] : null, + modified_date: moment().subtract(i, 'days').toISOString(), + linkedIndices: i % 2 === 0 ? [`index${i}`] : undefined, name: `testy${i}`, + policy: { + name: `testy${i}`, + phases: {}, + }, }); } jest.mock(''); -let component = null; +let component: ReactElement; -const snapshot = (rendered) => { +const snapshot = (rendered: string[]) => { expect(rendered).toMatchSnapshot(); }; -const mountedSnapshot = (rendered) => { +const mountedSnapshot = (rendered: ReactWrapper) => { expect(takeMountedSnapshot(rendered)).toMatchSnapshot(); }; -const names = (rendered) => { +const names = (rendered: ReactWrapper) => { return findTestSubject(rendered, 'policyTablePolicyNameLink'); }; -const namesText = (rendered) => { - return names(rendered).map((button) => button.text()); +const namesText = (rendered: ReactWrapper): string[] => { + return (names(rendered) as ReactWrapper).map((button) => button.text()); }; -const testSort = (headerName) => { +const testSort = (headerName: string) => { const rendered = mountWithIntl(component); const nameHeader = findTestSubject(rendered, `policyTableHeaderCell-${headerName}`).find( 'button' @@ -63,7 +71,7 @@ const testSort = (headerName) => { rendered.update(); snapshot(namesText(rendered)); }; -const openContextMenu = (buttonIndex) => { +const openContextMenu = (buttonIndex: number) => { const rendered = mountWithIntl(component); const actionsButton = findTestSubject(rendered, 'policyActionsContextMenuButton'); actionsButton.at(buttonIndex).simulate('click'); @@ -73,33 +81,26 @@ const openContextMenu = (buttonIndex) => { describe('policy table', () => { beforeEach(() => { - store = indexLifecycleManagementStore(); component = ( - - {}} /> - + ); - store.dispatch(fetchedPolicies(policies)); - server = sinon.fakeServer.create(); - server.respondWith('/api/index_lifecycle_management/policies', [ - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify(policies), - ]); }); - test('should show spinner when policies are loading', () => { - store = indexLifecycleManagementStore(); + + test('should show empty state when there are not any policies', () => { component = ( - - {}} /> - + ); const rendered = mountWithIntl(component); - expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy(); - }); - test('should show empty state when there are not any policies', () => { - store.dispatch(fetchedPolicies([])); - const rendered = mountWithIntl(component); mountedSnapshot(rendered); }); test('should change pages when a pagination link is clicked on', () => { @@ -123,7 +124,7 @@ describe('policy table', () => { test('should filter based on content of search input', () => { const rendered = mountWithIntl(component); const searchInput = rendered.find('.euiFieldSearch').first(); - searchInput.instance().value = 'testy0'; + ((searchInput.instance() as unknown) as HTMLInputElement).value = 'testy0'; searchInput.simulate('keyup', { key: 'Enter', keyCode: 13, which: 13 }); rendered.update(); snapshot(namesText(rendered)); @@ -147,7 +148,7 @@ describe('policy table', () => { expect(buttons.at(0).text()).toBe('View indices linked to policy'); expect(buttons.at(1).text()).toBe('Add policy to index template'); expect(buttons.at(2).text()).toBe('Delete policy'); - expect(buttons.at(2).getDOMNode().disabled).toBeTruthy(); + expect((buttons.at(2).getDOMNode() as HTMLButtonElement).disabled).toBeTruthy(); }); test('should have proper actions in context menu when there are not linked indices', () => { const rendered = openContextMenu(1); @@ -155,7 +156,7 @@ describe('policy table', () => { expect(buttons.length).toBe(2); expect(buttons.at(0).text()).toBe('Add policy to index template'); expect(buttons.at(1).text()).toBe('Delete policy'); - expect(buttons.at(1).getDOMNode().disabled).toBeFalsy(); + expect((buttons.at(1).getDOMNode() as HTMLButtonElement).disabled).toBeFalsy(); }); test('confirmation modal should show when delete button is pressed', () => { const rendered = openContextMenu(1); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx similarity index 88% rename from x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js rename to x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx index 4fa1838115840..17573cb81c408 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx @@ -8,7 +8,8 @@ import moment from 'moment-timezone'; import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { mountWithIntl } from '../../../test_utils/enzyme_helpers'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/public/mocks'; import { retryLifecycleActionExtension, removeLifecyclePolicyActionExtension, @@ -19,19 +20,22 @@ import { } from '../public/extend_index_management'; import { init as initHttp } from '../public/application/services/http'; import { init as initUiMetric } from '../public/application/services/ui_metric'; +import { Index } from '../public/application/services/policies/types'; // We need to init the http with a mock for any tests that depend upon the http service. // For example, add_lifecycle_confirm_modal makes an API request in its componentDidMount // lifecycle method. If we don't mock this, CI will fail with "Call retries were exceeded". -initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path); -initUiMetric({ reportUiStats: () => {} }); +// This expects HttpSetup but we're giving it AxiosInstance. +// @ts-ignore +initHttp(axios.create({ adapter: axiosXhrAdapter })); +initUiMetric(usageCollectionPluginMock.createSetupContract()); jest.mock('../../../plugins/index_management/public', async () => { - const { indexManagementMock } = await import('../../../plugins/index_management/public/mocks.ts'); + const { indexManagementMock } = await import('../../../plugins/index_management/public/mocks'); return indexManagementMock.createSetup(); }); -const indexWithoutLifecyclePolicy = { +const indexWithoutLifecyclePolicy: Index = { health: 'yellow', status: 'open', name: 'noPolicy', @@ -43,13 +47,14 @@ const indexWithoutLifecyclePolicy = { size: '3.4kb', primary_size: '3.4kb', aliases: 'none', + isFrozen: false, ilm: { index: 'testy1', managed: false, }, }; -const indexWithLifecyclePolicy = { +const indexWithLifecyclePolicy: Index = { health: 'yellow', status: 'open', name: 'testy3', @@ -61,6 +66,7 @@ const indexWithLifecyclePolicy = { size: '6.5kb', primary_size: '6.5kb', aliases: 'none', + isFrozen: false, ilm: { index: 'testy3', managed: true, @@ -87,6 +93,7 @@ const indexWithLifecycleError = { size: '6.5kb', primary_size: '6.5kb', aliases: 'none', + isFrozen: false, ilm: { index: 'testy3', managed: true, @@ -115,10 +122,12 @@ const indexWithLifecycleError = { moment.tz.setDefault('utc'); -const getUrlForApp = (appId, options) => { +const getUrlForApp = (appId: string, options: any) => { return appId + '/' + (options ? options.path : ''); }; +const reloadIndices = () => {}; + describe('extend index management', () => { describe('retry lifecycle action extension', () => { test('should return null when no indices have index lifecycle policy', () => { @@ -153,6 +162,7 @@ describe('extend index management', () => { test('should return null when no indices have index lifecycle policy', () => { const extension = removeLifecyclePolicyActionExtension({ indices: [indexWithoutLifecyclePolicy], + reloadIndices, }); expect(extension).toBeNull(); }); @@ -160,6 +170,7 @@ describe('extend index management', () => { test('should return null when some indices have index lifecycle policy', () => { const extension = removeLifecyclePolicyActionExtension({ indices: [indexWithoutLifecyclePolicy, indexWithLifecyclePolicy], + reloadIndices, }); expect(extension).toBeNull(); }); @@ -167,6 +178,7 @@ describe('extend index management', () => { test('should return extension when all indices have lifecycle policy', () => { const extension = removeLifecyclePolicyActionExtension({ indices: [indexWithLifecycleError, indexWithLifecycleError], + reloadIndices, }); expect(extension).toBeDefined(); expect(extension).toMatchSnapshot(); @@ -175,16 +187,18 @@ describe('extend index management', () => { describe('add lifecycle policy action extension', () => { test('should return null when index has index lifecycle policy', () => { - const extension = addLifecyclePolicyActionExtension( - { indices: [indexWithLifecyclePolicy] }, - getUrlForApp - ); + const extension = addLifecyclePolicyActionExtension({ + indices: [indexWithLifecyclePolicy], + reloadIndices, + getUrlForApp, + }); expect(extension).toBeNull(); }); test('should return null when more than one index is passed', () => { const extension = addLifecyclePolicyActionExtension({ indices: [indexWithoutLifecyclePolicy, indexWithoutLifecyclePolicy], + reloadIndices, getUrlForApp, }); expect(extension).toBeNull(); @@ -193,10 +207,11 @@ describe('extend index management', () => { test('should return extension when one index is passed and it does not have lifecycle policy', () => { const extension = addLifecyclePolicyActionExtension({ indices: [indexWithoutLifecyclePolicy], + reloadIndices, getUrlForApp, }); - expect(extension.renderConfirmModal).toBeDefined; - const component = extension.renderConfirmModal(jest.fn()); + expect(extension?.renderConfirmModal).toBeDefined(); + const component = extension!.renderConfirmModal(jest.fn()); const rendered = mountWithIntl(component); expect(rendered.exists('.euiModal--confirmation')); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx index 31a9abdc7145e..d7812f186a03f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -6,12 +6,10 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Provider } from 'react-redux'; import { I18nStart, ScopedHistory, ApplicationStart } from 'kibana/public'; import { UnmountCallback } from 'src/core/public'; import { App } from './app'; -import { indexLifecycleManagementStore } from './store'; export const renderApp = ( element: Element, @@ -22,9 +20,7 @@ export const renderApp = ( ): UnmountCallback => { render( - - - + , element ); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx index 359134e015f7f..f4697693b86c6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { EuiButton, EuiCallOut, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useLoadPoliciesList } from '../../services/api'; @@ -50,25 +50,29 @@ export const EditPolicy: React.FunctionComponent +

    + +

    } - color="danger" - > -

    - {message} ({statusCode}) -

    - - - - + body={ +

    + {message} ({statusCode}) +

    + } + actions={ + + + + } + /> ); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx similarity index 89% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx index 47134ad097720..90ac3c03856de 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx @@ -20,21 +20,35 @@ import { EuiText, } from '@elastic/eui'; -import { toasts } from '../../../../services/notification'; -import { addLifecyclePolicyToTemplate, loadIndexTemplates } from '../../../../services/api'; -import { showApiError } from '../../../../services/api_errors'; -import { LearnMoreLink } from '../../../edit_policy/components'; +import { LearnMoreLink } from '../../edit_policy/components'; +import { PolicyFromES } from '../../../services/policies/types'; +import { addLifecyclePolicyToTemplate, loadIndexTemplates } from '../../../services/api'; +import { toasts } from '../../../services/notification'; +import { showApiError } from '../../../services/api_errors'; -export class AddPolicyToTemplateConfirmModal extends Component { - state = { - templates: [], - }; +interface Props { + policy: PolicyFromES; + onCancel: () => void; +} +interface State { + templates: Array<{ name: string }>; + templateName?: string; + aliasName?: string; + templateError?: string; +} +export class AddPolicyToTemplateConfirmModal extends Component { + constructor(props: Props) { + super(props); + this.state = { + templates: [], + }; + } async componentDidMount() { const templates = await loadIndexTemplates(); this.setState({ templates }); } addPolicyToTemplate = async () => { - const { policy, callback, onCancel } = this.props; + const { policy, onCancel } = this.props; const { templateName, aliasName } = this.state; const policyName = policy.name; if (!templateName) { @@ -71,9 +85,6 @@ export class AddPolicyToTemplateConfirmModal extends Component { ); showApiError(e, title); } - if (callback) { - callback(); - } }; renderTemplateHasPolicyWarning() { const selectedTemplate = this.getSelectedTemplate(); @@ -144,7 +155,7 @@ export class AddPolicyToTemplateConfirmModal extends Component { options={options} value={templateName} onChange={(e) => { - this.setState({ templateError: null, templateName: e.target.value }); + this.setState({ templateError: undefined, templateName: e.target.value }); }} /> @@ -204,7 +215,6 @@ export class AddPolicyToTemplateConfirmModal extends Component { defaultMessage: 'Add policy', } )} - onClose={onCancel} >

    diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/confirm_delete.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx similarity index 85% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/confirm_delete.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx index 0ecc9cc13ecd0..8d8e5ac2a2472 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/confirm_delete.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx @@ -9,11 +9,17 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; -import { toasts } from '../../../../services/notification'; -import { deletePolicy } from '../../../../services/api'; -import { showApiError } from '../../../../services/api_errors'; +import { PolicyFromES } from '../../../services/policies/types'; +import { toasts } from '../../../services/notification'; +import { showApiError } from '../../../services/api_errors'; +import { deletePolicy } from '../../../services/api'; -export class ConfirmDelete extends Component { +interface Props { + policyToDelete: PolicyFromES; + callback: () => void; + onCancel: () => void; +} +export class ConfirmDelete extends Component { deletePolicy = async () => { const { policyToDelete, callback } = this.props; const policyName = policyToDelete.name; @@ -61,7 +67,6 @@ export class ConfirmDelete extends Component { /> } buttonColor="danger" - onClose={onCancel} >

    ( -
    - -
    -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.container.js deleted file mode 100644 index 8bd78774d2d55..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.container.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 { connect } from 'react-redux'; - -import { - fetchPolicies, - policyFilterChanged, - policyPageChanged, - policyPageSizeChanged, - policySortChanged, -} from '../../../../store/actions'; - -import { - getPolicies, - getPageOfPolicies, - getPolicyPager, - getPolicyFilter, - getPolicySort, - isPolicyListLoaded, -} from '../../../../store/selectors'; - -import { PolicyTable as PresentationComponent } from './policy_table'; - -const mapDispatchToProps = (dispatch) => { - return { - policyFilterChanged: (filter) => { - dispatch(policyFilterChanged({ filter })); - }, - policyPageChanged: (pageNumber) => { - dispatch(policyPageChanged({ pageNumber })); - }, - policyPageSizeChanged: (pageSize) => { - dispatch(policyPageSizeChanged({ pageSize })); - }, - policySortChanged: (sortField, isSortAscending) => { - dispatch(policySortChanged({ sortField, isSortAscending })); - }, - fetchPolicies: (withIndices) => { - dispatch(fetchPolicies(withIndices)); - }, - }; -}; - -export const PolicyTable = connect( - (state) => ({ - totalNumberOfPolicies: getPolicies(state).length, - policies: getPageOfPolicies(state), - pager: getPolicyPager(state), - filter: getPolicyFilter(state), - sortField: getPolicySort(state).sortField, - isSortAscending: getPolicySort(state).isSortAscending, - policyListLoaded: isPolicyListLoaded(state), - }), - mapDispatchToProps -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js deleted file mode 100644 index ec1cdb987f4b3..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js +++ /dev/null @@ -1,530 +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 React, { Component, Fragment } from 'react'; -import moment from 'moment-timezone'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { - EuiButton, - EuiButtonEmpty, - EuiLink, - EuiEmptyPrompt, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPopover, - EuiContextMenu, - EuiSpacer, - EuiTable, - EuiTableBody, - EuiTableHeader, - EuiTableHeaderCell, - EuiTablePagination, - EuiTableRow, - EuiTableRowCell, - EuiTitle, - EuiText, - EuiPageBody, - EuiPageContent, - EuiScreenReaderOnly, -} from '@elastic/eui'; - -import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; -import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { getIndexListUri } from '../../../../../../../index_management/public'; -import { UIM_EDIT_CLICK } from '../../../../constants/ui_metric'; -import { getPolicyPath } from '../../../../services/navigation'; -import { flattenPanelTree } from '../../../../services/flatten_panel_tree'; -import { trackUiMetric } from '../../../../services/ui_metric'; -import { NoMatch } from '../no_match'; -import { ConfirmDelete } from './confirm_delete'; -import { AddPolicyToTemplateConfirmModal } from './add_policy_to_template_confirm_modal'; - -const COLUMNS = { - name: { - label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.nameHeader', { - defaultMessage: 'Name', - }), - width: 200, - }, - linkedIndices: { - label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.linkedIndicesHeader', { - defaultMessage: 'Linked indices', - }), - width: 120, - }, - version: { - label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.versionHeader', { - defaultMessage: 'Version', - }), - width: 120, - }, - modified_date: { - label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.modifiedDateHeader', { - defaultMessage: 'Modified date', - }), - width: 200, - }, -}; - -export class PolicyTable extends Component { - constructor(props) { - super(props); - - this.state = { - selectedPoliciesMap: {}, - renderConfirmModal: null, - }; - } - componentDidMount() { - this.props.fetchPolicies(true); - } - renderEmpty() { - return ( - - - - } - body={ - -

    - -

    -
    - } - actions={this.renderCreatePolicyButton()} - /> - ); - } - renderDeleteConfirmModal = () => { - const { policyToDelete } = this.state; - if (!policyToDelete) { - return null; - } - return ( - this.setState({ renderConfirmModal: null, policyToDelete: null })} - /> - ); - }; - renderAddPolicyToTemplateConfirmModal = () => { - const { policyToAddToTemplate } = this.state; - if (!policyToAddToTemplate) { - return null; - } - return ( - this.setState({ renderConfirmModal: null, policyToAddToTemplate: null })} - /> - ); - }; - handleDelete = () => { - this.props.fetchPolicies(true); - this.setState({ renderDeleteConfirmModal: null, policyToDelete: null }); - }; - onSort = (column) => { - const { sortField, isSortAscending, policySortChanged } = this.props; - const newIsSortAscending = sortField === column ? !isSortAscending : true; - policySortChanged(column, newIsSortAscending); - }; - - buildHeader() { - const { sortField, isSortAscending } = this.props; - const headers = Object.entries(COLUMNS).map(([fieldName, { label, width }]) => { - const isSorted = sortField === fieldName; - return ( - this.onSort(fieldName)} - isSorted={isSorted} - isSortAscending={isSortAscending} - data-test-subj={`policyTableHeaderCell-${fieldName}`} - className={'policyTable__header--' + fieldName} - width={width} - > - {label} - - ); - }); - headers.push( - - ); - return headers; - } - - buildRowCell(fieldName, value) { - if (fieldName === 'name') { - return ( - /* eslint-disable-next-line @elastic/eui/href-or-on-click */ - - trackUiMetric('click', UIM_EDIT_CLICK) - )} - > - {value} - - ); - } else if (fieldName === 'linkedIndices') { - return ( - - {value ? value.length : '0'} - - ); - } else if (fieldName === 'modified_date' && value) { - return moment(value).format('YYYY-MM-DD HH:mm:ss'); - } - return value; - } - renderCreatePolicyButton() { - return ( - - - - ); - } - renderConfirmModal() { - const { renderConfirmModal } = this.state; - if (renderConfirmModal) { - return renderConfirmModal(); - } else { - return null; - } - } - buildActionPanelTree(policy) { - const hasLinkedIndices = Boolean(policy.linkedIndices && policy.linkedIndices.length); - - const viewIndicesLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.viewIndicesButtonText', - { - defaultMessage: 'View indices linked to policy', - } - ); - const addPolicyToTemplateLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText', - { - defaultMessage: 'Add policy to index template', - } - ); - const deletePolicyLabel = i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText', - { - defaultMessage: 'Delete policy', - } - ); - const deletePolicyTooltip = hasLinkedIndices - ? i18n.translate('xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip', { - defaultMessage: 'You cannot delete a policy that is being used by an index', - }) - : null; - const items = []; - if (hasLinkedIndices) { - items.push({ - name: viewIndicesLabel, - icon: 'list', - onClick: () => { - this.props.navigateToApp('management', { - path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`, true)}`, - }); - }, - }); - } - items.push({ - name: addPolicyToTemplateLabel, - icon: 'plusInCircle', - onClick: () => - this.setState({ - renderConfirmModal: this.renderAddPolicyToTemplateConfirmModal, - policyToAddToTemplate: policy, - }), - }); - items.push({ - name: deletePolicyLabel, - disabled: hasLinkedIndices, - icon: 'trash', - toolTipContent: deletePolicyTooltip, - onClick: () => - this.setState({ - renderConfirmModal: this.renderDeleteConfirmModal, - policyToDelete: policy, - }), - }); - const panelTree = { - id: 0, - title: i18n.translate('xpack.indexLifecycleMgmt.policyTable.policyActionsMenu.panelTitle', { - defaultMessage: 'Policy options', - }), - items, - }; - return flattenPanelTree(panelTree); - } - togglePolicyPopover = (policy) => { - if (this.isPolicyPopoverOpen(policy)) { - this.closePolicyPopover(policy); - } else { - this.openPolicyPopover(policy); - } - }; - isPolicyPopoverOpen = (policy) => { - return this.state.policyPopover === policy.name; - }; - closePolicyPopover = (policy) => { - if (this.isPolicyPopoverOpen(policy)) { - this.setState({ policyPopover: null }); - } - }; - openPolicyPopover = (policy) => { - this.setState({ policyPopover: policy.name }); - }; - buildRowCells(policy) { - const { name } = policy; - const cells = Object.entries(COLUMNS).map(([fieldName, { width }]) => { - const value = policy[fieldName]; - - if (fieldName === 'name') { - return ( - -
    - {this.buildRowCell(fieldName, value)} -
    - - ); - } - - return ( - - {this.buildRowCell(fieldName, value)} - - ); - }); - const button = ( - this.togglePolicyPopover(policy)} - color="primary" - > - {i18n.translate('xpack.indexLifecycleMgmt.policyTable.actionsButtonText', { - defaultMessage: 'Actions', - })} - - ); - cells.push( - - this.closePolicyPopover(policy)} - panelPaddingSize="none" - withTitle - anchorPosition="rightUp" - repositionOnScroll - > - - - - ); - return cells; - } - - buildRows() { - const { policies = [] } = this.props; - return policies.map((policy) => { - const { name } = policy; - return {this.buildRowCells(policy)}; - }); - } - - renderPager() { - const { pager, policyPageChanged, policyPageSizeChanged } = this.props; - return ( - - ); - } - - onItemSelectionChanged = (selectedPolicies) => { - this.setState({ selectedPolicies }); - }; - - render() { - const { - totalNumberOfPolicies, - policyFilterChanged, - filter, - policyListLoaded, - policies, - } = this.props; - const { selectedPoliciesMap } = this.state; - const numSelected = Object.keys(selectedPoliciesMap).length; - let content; - let tableContent; - if (totalNumberOfPolicies || !policyListLoaded) { - if (!policyListLoaded) { - tableContent = ; - } else if (totalNumberOfPolicies > 0) { - tableContent = ( - - - - - - - {this.buildHeader()} - {this.buildRows()} - - ); - } else { - tableContent = ; - } - content = ( - - - {numSelected > 0 ? ( - - this.setState({ showDeleteConfirmation: true })} - > - - - - ) : null} - - { - policyFilterChanged(event.target.value); - }} - data-test-subj="policyTableFilterInput" - placeholder={i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.systempoliciesSearchInputPlaceholder', - { - defaultMessage: 'Search', - } - )} - aria-label={i18n.translate( - 'xpack.indexLifecycleMgmt.policyTable.systempoliciesSearchInputAriaLabel', - { - defaultMessage: 'Search policies', - } - )} - /> - - - - {tableContent} - - ); - } else { - content = this.renderEmpty(); - } - - return ( - - -
    - {this.renderConfirmModal()} - {totalNumberOfPolicies || !policyListLoaded ? ( - - - - -

    - -

    -
    -
    - {totalNumberOfPolicies ? ( - {this.renderCreatePolicyButton()} - ) : null} -
    - - -

    - -

    -
    -
    - ) : null} - - {content} - - {totalNumberOfPolicies && totalNumberOfPolicies > 10 ? this.renderPager() : null} -
    -
    -
    - ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx new file mode 100644 index 0000000000000..da36ff4df98f5 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx @@ -0,0 +1,376 @@ +/* + * 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 React, { ReactElement, useState, Fragment, ReactNode } from 'react'; +import { + EuiButtonEmpty, + EuiContextMenu, + EuiLink, + EuiPopover, + EuiScreenReaderOnly, + EuiSpacer, + EuiTable, + EuiTableBody, + EuiTableHeader, + EuiTableHeaderCell, + EuiTablePagination, + EuiTableRow, + EuiTableRowCell, + EuiText, + Pager, + EuiContextMenuPanelDescriptor, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; + +import moment from 'moment'; +import { ApplicationStart } from 'kibana/public'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { RouteComponentProps } from 'react-router-dom'; +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; +import { getIndexListUri } from '../../../../../../index_management/public'; +import { PolicyFromES } from '../../../services/policies/types'; +import { getPolicyPath } from '../../../services/navigation'; +import { sortTable } from '../../../services'; +import { trackUiMetric } from '../../../services/ui_metric'; + +import { UIM_EDIT_CLICK } from '../../../constants'; +import { AddPolicyToTemplateConfirmModal } from './add_policy_to_template_confirm_modal'; +import { ConfirmDelete } from './confirm_delete'; + +type PolicyProperty = Extract< + keyof PolicyFromES, + 'version' | 'name' | 'linkedIndices' | 'modified_date' +>; +const COLUMNS: Array<[PolicyProperty, { label: string; width: number }]> = [ + [ + 'name', + { + label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.nameHeader', { + defaultMessage: 'Name', + }), + width: 200, + }, + ], + [ + 'linkedIndices', + { + label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.linkedIndicesHeader', { + defaultMessage: 'Linked indices', + }), + width: 120, + }, + ], + [ + 'version', + { + label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.versionHeader', { + defaultMessage: 'Version', + }), + width: 120, + }, + ], + [ + 'modified_date', + { + label: i18n.translate('xpack.indexLifecycleMgmt.policyTable.headers.modifiedDateHeader', { + defaultMessage: 'Modified date', + }), + width: 200, + }, + ], +]; + +interface Props { + policies: PolicyFromES[]; + totalNumber: number; + navigateToApp: ApplicationStart['navigateToApp']; + setConfirmModal: (modal: ReactElement | null) => void; + handleDelete: () => void; + history: RouteComponentProps['history']; +} +export const TableContent: React.FunctionComponent = ({ + policies, + totalNumber, + navigateToApp, + setConfirmModal, + handleDelete, + history, +}) => { + const [popoverPolicy, setPopoverPolicy] = useState(); + const [sort, setSort] = useState<{ sortField: PolicyProperty; isSortAscending: boolean }>({ + sortField: 'name', + isSortAscending: true, + }); + const [pageSize, setPageSize] = useState(10); + const [currentPage, setCurrentPage] = useState(0); + + let sortedPolicies = sortTable(policies, sort.sortField, sort.isSortAscending); + const pager = new Pager(totalNumber, pageSize, currentPage); + const { firstItemIndex, lastItemIndex } = pager; + sortedPolicies = sortedPolicies.slice(firstItemIndex, lastItemIndex + 1); + + const isPolicyPopoverOpen = (policyName: string): boolean => popoverPolicy === policyName; + const closePolicyPopover = (): void => { + setPopoverPolicy(''); + }; + const openPolicyPopover = (policyName: string): void => { + setPopoverPolicy(policyName); + }; + const togglePolicyPopover = (policyName: string): void => { + if (isPolicyPopoverOpen(policyName)) { + closePolicyPopover(); + } else { + openPolicyPopover(policyName); + } + }; + + const onSort = (column: PolicyProperty) => { + const newIsSortAscending = sort.sortField === column ? !sort.isSortAscending : true; + setSort({ sortField: column, isSortAscending: newIsSortAscending }); + }; + + const headers = []; + COLUMNS.forEach(([fieldName, { label, width }]) => { + const isSorted = sort.sortField === fieldName; + headers.push( + onSort(fieldName)} + isSorted={isSorted} + isSortAscending={sort.isSortAscending} + data-test-subj={`policyTableHeaderCell-${fieldName}`} + className={'policyTable__header--' + fieldName} + width={width} + > + {label} + + ); + }); + headers.push( + + ); + + const buildActionPanelTree = (policy: PolicyFromES): EuiContextMenuPanelDescriptor[] => { + const hasLinkedIndices = Boolean(policy.linkedIndices && policy.linkedIndices.length); + + const viewIndicesLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.viewIndicesButtonText', + { + defaultMessage: 'View indices linked to policy', + } + ); + const addPolicyToTemplateLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText', + { + defaultMessage: 'Add policy to index template', + } + ); + const deletePolicyLabel = i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText', + { + defaultMessage: 'Delete policy', + } + ); + const deletePolicyTooltip = hasLinkedIndices + ? i18n.translate('xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip', { + defaultMessage: 'You cannot delete a policy that is being used by an index', + }) + : null; + const items = []; + if (hasLinkedIndices) { + items.push({ + name: viewIndicesLabel, + icon: 'list', + onClick: () => { + navigateToApp('management', { + path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`, true)}`, + }); + }, + }); + } + items.push({ + name: addPolicyToTemplateLabel, + icon: 'plusInCircle', + onClick: () => { + setConfirmModal(renderAddPolicyToTemplateConfirmModal(policy)); + }, + }); + items.push({ + name: deletePolicyLabel, + disabled: hasLinkedIndices, + icon: 'trash', + toolTipContent: deletePolicyTooltip, + onClick: () => { + setConfirmModal(renderDeleteConfirmModal(policy)); + }, + }); + const panelTree = { + id: 0, + title: i18n.translate('xpack.indexLifecycleMgmt.policyTable.policyActionsMenu.panelTitle', { + defaultMessage: 'Policy options', + }), + items, + }; + return [panelTree]; + }; + + const renderRowCell = (fieldName: string, value: string | number | string[]): ReactNode => { + if (fieldName === 'name') { + return ( + + trackUiMetric(METRIC_TYPE.CLICK, UIM_EDIT_CLICK) + )} + > + {value} + + ); + } else if (fieldName === 'linkedIndices') { + return ( + + {value ? (value as string[]).length : '0'} + + ); + } else if (fieldName === 'modified_date' && value) { + return moment(value).format('YYYY-MM-DD HH:mm:ss'); + } + return value; + }; + + const renderRowCells = (policy: PolicyFromES): ReactElement[] => { + const { name } = policy; + const cells = []; + COLUMNS.forEach(([fieldName, { width }]) => { + const value: any = policy[fieldName]; + + if (fieldName === 'name') { + cells.push( + +
    + {renderRowCell(fieldName, value)} +
    + + ); + } else { + cells.push( + + {renderRowCell(fieldName, value)} + + ); + } + }); + const button = ( + togglePolicyPopover(policy.name)} + color="primary" + > + {i18n.translate('xpack.indexLifecycleMgmt.policyTable.actionsButtonText', { + defaultMessage: 'Actions', + })} + + ); + cells.push( + + + + + + ); + return cells; + }; + + const rows = sortedPolicies.map((policy) => { + const { name } = policy; + return {renderRowCells(policy)}; + }); + + const renderAddPolicyToTemplateConfirmModal = (policy: PolicyFromES): ReactElement => { + return ( + setConfirmModal(null)} /> + ); + }; + + const renderDeleteConfirmModal = (policy: PolicyFromES): ReactElement => { + return ( + { + setConfirmModal(null); + }} + /> + ); + }; + + const renderPager = (): ReactNode => { + return ( + + ); + }; + + return ( + + + + + + + + {headers} + {rows} + + + {policies.length > 10 ? renderPager() : null} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/index.ts diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx new file mode 100644 index 0000000000000..f6471ff2da4d3 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.container.tsx @@ -0,0 +1,75 @@ +/* + * 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 React from 'react'; +import { ApplicationStart } from 'kibana/public'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { PolicyTable as PresentationComponent } from './policy_table'; +import { useLoadPoliciesList } from '../../services/api'; + +interface Props { + navigateToApp: ApplicationStart['navigateToApp']; +} + +export const PolicyTable: React.FunctionComponent = ({ + navigateToApp, + history, +}) => { + const { data: policies, isLoading, error, sendRequest } = useLoadPoliciesList(true); + + if (isLoading) { + return ( + } + body={ + + } + /> + ); + } + if (error) { + const { statusCode, message } = error ? error : { statusCode: '', message: '' }; + return ( + + + + } + body={ +

    + {message} ({statusCode}) +

    + } + actions={ + + + + } + /> + ); + } + + return ( + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx new file mode 100644 index 0000000000000..048ab922a65b5 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx @@ -0,0 +1,183 @@ +/* + * 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 React, { Fragment, ReactElement, ReactNode, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiButton, + EuiEmptyPrompt, + EuiFieldSearch, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, + EuiText, + EuiPageBody, + EuiPageContent, +} from '@elastic/eui'; +import { ApplicationStart } from 'kibana/public'; +import { RouteComponentProps } from 'react-router-dom'; +import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; +import { PolicyFromES } from '../../services/policies/types'; +import { filterItems } from '../../services'; +import { TableContent } from './components/table_content'; + +interface Props { + policies: PolicyFromES[]; + history: RouteComponentProps['history']; + navigateToApp: ApplicationStart['navigateToApp']; + updatePolicies: () => void; +} + +export const PolicyTable: React.FunctionComponent = ({ + policies, + history, + navigateToApp, + updatePolicies, +}) => { + const [confirmModal, setConfirmModal] = useState(); + const [filter, setFilter] = useState(''); + + const createPolicyButton = ( + + + + ); + + let content: ReactElement; + + if (policies.length > 0) { + const filteredPolicies = filterItems('name', filter, policies); + let tableContent: ReactElement; + if (filteredPolicies.length > 0) { + tableContent = ( + { + updatePolicies(); + setConfirmModal(null); + }} + history={history} + /> + ); + } else { + tableContent = ( + + ); + } + + content = ( + + + + { + setFilter(event.target.value); + }} + data-test-subj="policyTableFilterInput" + placeholder={i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.systempoliciesSearchInputPlaceholder', + { + defaultMessage: 'Search', + } + )} + aria-label={i18n.translate( + 'xpack.indexLifecycleMgmt.policyTable.systempoliciesSearchInputAriaLabel', + { + defaultMessage: 'Search policies', + } + )} + /> + + + + {tableContent} + + ); + } else { + return ( + + + + + + } + body={ + +

    + +

    +
    + } + actions={createPolicyButton} + /> +
    +
    + ); + } + + return ( + + + {confirmModal} + + + + +

    + +

    +
    +
    + {createPolicyButton} +
    + + +

    + +

    +
    + + + {content} +
    +
    + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index b80e9e70c54fa..e9365bfe06ea4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -12,7 +12,7 @@ import { UIM_POLICY_ATTACH_INDEX_TEMPLATE, UIM_POLICY_DETACH_INDEX, UIM_INDEX_RETRY_STEP, -} from '../constants/ui_metric'; +} from '../constants'; import { trackUiMetric } from './ui_metric'; import { sendGet, sendPost, sendDelete, useRequest } from './http'; @@ -78,7 +78,11 @@ export const removeLifecycleForIndex = async (indexNames: string[]) => { return response; }; -export const addLifecyclePolicyToIndex = async (body: GenericObject) => { +export const addLifecyclePolicyToIndex = async (body: { + indexName: string; + policyName: string; + alias: string; +}) => { const response = await sendPost(`index/add`, body); // Only track successful actions. trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_ATTACH_INDEX); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.js b/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.js deleted file mode 100644 index dcc9036463b82..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.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. - */ - -export const filterItems = (fields, filter = '', items = []) => { - const lowerFilter = filter.toLowerCase(); - return items.filter((item) => { - const actualFields = fields || Object.keys(item); - const indexOfMatch = actualFields.findIndex((field) => { - const normalizedField = String(item[field]).toLowerCase(); - return normalizedField.includes(lowerFilter); - }); - return indexOfMatch !== -1; - }); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.ts new file mode 100644 index 0000000000000..237ce567707bb --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/filter_items.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. + */ + +export const filterItems = (field: keyof T, filter: string, items: T[] = []): T[] => { + const lowerFilter = filter.toLowerCase(); + return items.filter((item: T) => { + const normalizedValue = String(item[field]).toLowerCase(); + return normalizedValue.includes(lowerFilter); + }); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/flatten_panel_tree.js b/x-pack/plugins/index_lifecycle_management/public/application/services/flatten_panel_tree.js deleted file mode 100644 index 2bb3903a6ef45..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/flatten_panel_tree.js +++ /dev/null @@ -1,20 +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 const flattenPanelTree = (tree, array = []) => { - array.push(tree); - - if (tree.items) { - tree.items.forEach((item) => { - if (item.panel) { - flattenPanelTree(item.panel, array); - item.panel = item.panel.id; - } - }); - } - - return array; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/index.js b/x-pack/plugins/index_lifecycle_management/public/application/services/index.ts similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/services/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/services/index.ts diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts index 3d4c73cf4a82c..0e00b5a02b71d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Index as IndexInterface } from '../../../../../index_management/public'; + export interface SerializedPolicy { name: string; phases: Phases; @@ -22,6 +24,7 @@ export interface PolicyFromES { name: string; policy: SerializedPolicy; version: number; + linkedIndices?: string[]; } export interface SerializedPhase { @@ -168,3 +171,36 @@ export interface FrozenPhase export interface DeletePhase extends CommonPhaseSettings, PhaseWithMinAge { waitForSnapshotPolicy: string; } + +export interface IndexLifecyclePolicy { + index: string; + managed: boolean; + action?: string; + action_time_millis?: number; + age?: string; + failed_step?: string; + failed_step_retry_count?: number; + is_auto_retryable_error?: boolean; + lifecycle_date_millis?: number; + phase?: string; + phase_execution?: { + policy: string; + modified_date_in_millis: number; + version: number; + phase_definition: SerializedPhase; + }; + phase_time_millis?: number; + policy?: string; + step?: string; + step_info?: { + reason?: string; + stack_trace?: string; + type?: string; + message?: string; + }; + step_time_millis?: number; +} + +export interface Index extends IndexInterface { + ilm: IndexLifecyclePolicy; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.js b/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.js deleted file mode 100644 index 1b1446bb735c1..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.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 { sortBy } from 'lodash'; - -const stringSort = (fieldName) => (item) => item[fieldName]; -const arraySort = (fieldName) => (item) => (item[fieldName] || []).length; - -const sorters = { - version: stringSort('version'), - name: stringSort('name'), - linkedIndices: arraySort('linkedIndices'), - modified_date: stringSort('modified_date'), -}; -export const sortTable = (array = [], sortField, isSortAscending) => { - const sorted = sortBy(array, sorters[sortField]); - return isSortAscending ? sorted : sorted.reverse(); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts new file mode 100644 index 0000000000000..6b41d671b673f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts @@ -0,0 +1,23 @@ +/* + * 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 { sortBy } from 'lodash'; +import { PolicyFromES } from './policies/types'; + +export const sortTable = ( + array: PolicyFromES[] = [], + sortField: Extract, + isSortAscending: boolean +): PolicyFromES[] => { + let sorter; + if (sortField === 'linkedIndices') { + sorter = (item: PolicyFromES) => (item[sortField] || []).length; + } else { + sorter = (item: PolicyFromES) => item[sortField]; + } + const sorted = sortBy(array, sorter); + return isSortAscending ? sorted : sorted.reverse(); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js deleted file mode 100644 index d47136679604f..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/policies.js +++ /dev/null @@ -1,42 +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'; -import { createAction } from 'redux-actions'; - -import { showApiError } from '../../services/api_errors'; -import { loadPolicies } from '../../services/api'; - -export const fetchedPolicies = createAction('FETCHED_POLICIES'); -export const setSelectedPolicy = createAction('SET_SELECTED_POLICY'); -export const unsetSelectedPolicy = createAction('UNSET_SELECTED_POLICY'); -export const setSelectedPolicyName = createAction('SET_SELECTED_POLICY_NAME'); -export const setSaveAsNewPolicy = createAction('SET_SAVE_AS_NEW_POLICY'); -export const policySortChanged = createAction('POLICY_SORT_CHANGED'); -export const policyPageSizeChanged = createAction('POLICY_PAGE_SIZE_CHANGED'); -export const policyPageChanged = createAction('POLICY_PAGE_CHANGED'); -export const policySortDirectionChanged = createAction('POLICY_SORT_DIRECTION_CHANGED'); -export const policyFilterChanged = createAction('POLICY_FILTER_CHANGED'); - -export const fetchPolicies = (withIndices, callback) => async (dispatch) => { - let policies; - try { - policies = await loadPolicies(withIndices); - } catch (err) { - const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.loadPolicyErrorMessage', { - defaultMessage: 'Error loading policies', - }); - showApiError(err, title); - return false; - } - - dispatch(fetchedPolicies(policies)); - if (policies.length === 0) { - dispatch(setSelectedPolicy()); - } - callback && callback(); - return policies; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/application/store/index.d.ts deleted file mode 100644 index 8617a7045a5c3..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/index.d.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 declare const indexLifecycleManagementStore: any; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/index.js b/x-pack/plugins/index_lifecycle_management/public/application/store/index.js deleted file mode 100644 index 808eb489bf913..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/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 { indexLifecycleManagementStore } from './store'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js deleted file mode 100644 index ca9d59e295a29..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/policies.js +++ /dev/null @@ -1,76 +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 { handleActions } from 'redux-actions'; -import { - fetchedPolicies, - policyFilterChanged, - policyPageChanged, - policyPageSizeChanged, - policySortChanged, -} from '../actions'; - -const defaultState = { - isLoading: false, - isLoaded: false, - originalPolicyName: undefined, - selectedPolicySet: false, - policies: [], - sort: { - sortField: 'name', - isSortAscending: true, - }, - pageSize: 10, - currentPage: 0, - filter: '', -}; - -export const policies = handleActions( - { - [fetchedPolicies](state, { payload: policies }) { - return { - ...state, - isLoading: false, - isLoaded: true, - policies, - }; - }, - [policyFilterChanged](state, action) { - const { filter } = action.payload; - return { - ...state, - filter, - currentPage: 0, - }; - }, - [policySortChanged](state, action) { - const { sortField, isSortAscending } = action.payload; - - return { - ...state, - sort: { - sortField, - isSortAscending, - }, - }; - }, - [policyPageChanged](state, action) { - const { pageNumber } = action.payload; - return { - ...state, - currentPage: pageNumber, - }; - }, - [policyPageSizeChanged](state, action) { - const { pageSize } = action.payload; - return { - ...state, - pageSize, - }; - }, - }, - defaultState -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js deleted file mode 100644 index e1c89314a2ec5..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js +++ /dev/null @@ -1,42 +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 { createSelector } from 'reselect'; -import { Pager } from '@elastic/eui'; - -import { filterItems, sortTable } from '../../services'; - -export const getPolicies = (state) => state.policies.policies; -export const getPolicyFilter = (state) => state.policies.filter; -export const getPolicySort = (state) => state.policies.sort; -export const getPolicyCurrentPage = (state) => state.policies.currentPage; -export const getPolicyPageSize = (state) => state.policies.pageSize; -export const isPolicyListLoaded = (state) => state.policies.isLoaded; - -const getFilteredPolicies = createSelector(getPolicies, getPolicyFilter, (policies, filter) => { - return filterItems(['name'], filter, policies); -}); -export const getTotalPolicies = createSelector(getFilteredPolicies, (filteredPolicies) => { - return filteredPolicies.length; -}); -export const getPolicyPager = createSelector( - getPolicyCurrentPage, - getPolicyPageSize, - getTotalPolicies, - (currentPage, pageSize, totalPolicies) => { - return new Pager(totalPolicies, pageSize, currentPage); - } -); -export const getPageOfPolicies = createSelector( - getFilteredPolicies, - getPolicySort, - getPolicyPager, - (filteredPolicies, sort, pager) => { - const sortedPolicies = sortTable(filteredPolicies, sort.sortField, sort.isSortAscending); - const { firstItemIndex, lastItemIndex } = pager; - return sortedPolicies.slice(firstItemIndex, lastItemIndex + 1); - } -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/store.js b/x-pack/plugins/index_lifecycle_management/public/application/store/store.js deleted file mode 100644 index c5774a3da238a..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/store.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 { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; - -import { indexLifecycleManagement } from './reducers/'; - -export const indexLifecycleManagementStore = (initialState = {}) => { - const enhancers = [applyMiddleware(thunk)]; - - window.__REDUX_DEVTOOLS_EXTENSION__ && enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__()); - return createStore(indexLifecycleManagement, initialState, compose(...enhancers)); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx similarity index 88% rename from x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx index 0bd313c9a9f8d..060b208006bf3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx @@ -8,6 +8,8 @@ import React, { Component, Fragment } from 'react'; import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ApplicationStart } from 'kibana/public'; + import { EuiLink, EuiSelect, @@ -26,9 +28,25 @@ import { import { loadPolicies, addLifecyclePolicyToIndex } from '../../application/services/api'; import { showApiError } from '../../application/services/api_errors'; import { toasts } from '../../application/services/notification'; +import { Index, PolicyFromES } from '../../application/services/policies/types'; + +interface Props { + indexName: string; + closeModal: () => void; + index: Index; + reloadIndices: () => void; + getUrlForApp: ApplicationStart['getUrlForApp']; +} + +interface State { + selectedPolicyName: string; + selectedAlias: string; + policies: PolicyFromES[]; + policyErrorMessage?: string; +} -export class AddLifecyclePolicyConfirmModal extends Component { - constructor(props) { +export class AddLifecyclePolicyConfirmModal extends Component { + constructor(props: Props) { super(props); this.state = { policies: [], @@ -41,7 +59,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { const { selectedPolicyName, selectedAlias } = this.state; if (!selectedPolicyName) { this.setState({ - policyError: i18n.translate( + policyErrorMessage: i18n.translate( 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.noPolicySelectedErrorMessage', { defaultMessage: 'You must select a policy.' } ), @@ -81,7 +99,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { ); } }; - renderAliasFormElement = (selectedPolicy) => { + renderAliasFormElement = (selectedPolicy?: PolicyFromES) => { const { selectedAlias } = this.state; const { index } = this.props; const showAliasSelect = @@ -109,7 +127,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { defaultMessage="Policy {policyName} is configured for rollover, but index {indexName} does not have an alias, which is required for rollover." values={{ - policyName: selectedPolicy.name, + policyName: selectedPolicy?.name, indexName: index.name, }} /> @@ -117,7 +135,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { ); } - const aliasOptions = aliases.map((alias) => { + const aliasOptions = (aliases as string[]).map((alias: string) => { return { text: alias, value: alias, @@ -152,10 +170,10 @@ export class AddLifecyclePolicyConfirmModal extends Component { ); }; renderForm() { - const { policies, selectedPolicyName, policyError } = this.state; + const { policies, selectedPolicyName, policyErrorMessage } = this.state; const selectedPolicy = selectedPolicyName ? policies.find((policy) => policy.name === selectedPolicyName) - : null; + : undefined; const options = policies.map(({ name }) => { return { @@ -175,8 +193,8 @@ export class AddLifecyclePolicyConfirmModal extends Component { return ( { - this.setState({ policyError: null, selectedPolicyName: e.target.value }); + this.setState({ policyErrorMessage: undefined, selectedPolicyName: e.target.value }); }} /> @@ -198,7 +216,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { } async componentDidMount() { try { - const policies = await loadPolicies(false, this.props.httpClient); + const policies = await loadPolicies(false); this.setState({ policies }); } catch (err) { showApiError( diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx similarity index 69% rename from x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx index 9e9dc009e4c40..02e4595a333bc 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx @@ -24,44 +24,71 @@ import { EuiPopoverTitle, } from '@elastic/eui'; +import { ApplicationStart } from 'kibana/public'; import { getPolicyPath } from '../../application/services/navigation'; +import { Index, IndexLifecyclePolicy } from '../../application/services/policies/types'; -const getHeaders = () => { - return { - policy: i18n.translate( - 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.lifecyclePolicyHeader', - { - defaultMessage: 'Lifecycle policy', - } - ), - phase: i18n.translate( - 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentPhaseHeader', - { - defaultMessage: 'Current phase', - } - ), - action: i18n.translate( - 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionHeader', - { - defaultMessage: 'Current action', - } - ), - action_time_millis: i18n.translate( - 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionTimeHeader', - { - defaultMessage: 'Current action time', - } - ), - failed_step: i18n.translate( - 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.failedStepHeader', - { - defaultMessage: 'Failed step', - } - ), - }; +const getHeaders = (): Array<[keyof IndexLifecyclePolicy, string]> => { + return [ + [ + 'policy', + i18n.translate( + 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.lifecyclePolicyHeader', + { + defaultMessage: 'Lifecycle policy', + } + ), + ], + [ + 'phase', + i18n.translate( + 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentPhaseHeader', + { + defaultMessage: 'Current phase', + } + ), + ], + [ + 'action', + i18n.translate( + 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionHeader', + { + defaultMessage: 'Current action', + } + ), + ], + [ + 'action_time_millis', + i18n.translate( + 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionTimeHeader', + { + defaultMessage: 'Current action time', + } + ), + ], + [ + 'failed_step', + i18n.translate( + 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.failedStepHeader', + { + defaultMessage: 'Failed step', + } + ), + ], + ]; }; -export class IndexLifecycleSummary extends Component { - constructor(props) { + +interface Props { + index: Index; + getUrlForApp: ApplicationStart['getUrlForApp']; +} +interface State { + showStackPopover: boolean; + showPhaseExecutionPopover: boolean; +} + +export class IndexLifecycleSummary extends Component { + constructor(props: Props) { super(props); this.state = { showStackPopover: false, @@ -80,8 +107,8 @@ export class IndexLifecycleSummary extends Component { closePhaseExecutionPopover = () => { this.setState({ showPhaseExecutionPopover: false }); }; - renderStackPopoverButton(ilm) { - if (!ilm.stack_trace) { + renderStackPopoverButton(ilm: IndexLifecyclePolicy) { + if (!ilm.step_info!.stack_trace) { return null; } const button = ( @@ -100,15 +127,12 @@ export class IndexLifecycleSummary extends Component { closePopover={this.closeStackPopover} >
    -
    {ilm.step_info.stack_trace}
    +
    {ilm.step_info!.stack_trace}
    ); } - renderPhaseExecutionPopoverButton(ilm) { - if (!ilm.phase_execution) { - return null; - } + renderPhaseExecutionPopoverButton(ilm: IndexLifecyclePolicy) { const button = ( { - const value = ilm[fieldName]; + headers.forEach(([fieldName, label], arrayIndex) => { + const value: any = ilm[fieldName]; let content; if (fieldName === 'action_time_millis') { content = moment(value).format('YYYY-MM-DD HH:mm:ss'); @@ -176,34 +203,38 @@ export class IndexLifecycleSummary extends Component { content = value; } content = content || '-'; - const cell = [ - - {headers[fieldName]} - , - - {content} - , - ]; + const cell = ( + <> + + {label} + + + {content} + + + ); if (arrayIndex % 2 === 0) { rows.left.push(cell); } else { rows.right.push(cell); } }); - rows.right.push(this.renderPhaseExecutionPopoverButton(ilm)); + if (ilm.phase_execution) { + rows.right.push(this.renderPhaseExecutionPopoverButton(ilm)); + } return rows; } render() { const { - index: { ilm = {} }, + index: { ilm }, } = this.props; if (!ilm.managed) { return null; } const { left, right } = this.buildRows(); return ( - + <>

    {ilm.step_info && ilm.step_info.type ? ( - + <> {this.renderStackPopoverButton(ilm)} - + ) : null} - {ilm.step_info && ilm.step_info.message && !ilm.step_info.stack_trace ? ( - + {ilm.step_info && ilm.step_info!.message && !ilm.step_info!.stack_trace ? ( + <> } > - {ilm.step_info.message} + {ilm.step_info!.message} - + ) : null} @@ -256,7 +287,7 @@ export class IndexLifecycleSummary extends Component { {right} - + ); } } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.ts b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.ts deleted file mode 100644 index 086ba67703122..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.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 declare const addAllExtensions: any; diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx similarity index 81% rename from x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx index 8d01f4a4c200e..bb5642cf3a476 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx @@ -8,22 +8,27 @@ import React from 'react'; import { get, every, some } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiSearchBar } from '@elastic/eui'; +import { ApplicationStart } from 'kibana/public'; + +import { IndexManagementPluginSetup } from '../../../index_management/public'; import { retryLifecycleForIndex } from '../application/services/api'; import { IndexLifecycleSummary } from './components/index_lifecycle_summary'; + import { AddLifecyclePolicyConfirmModal } from './components/add_lifecycle_confirm_modal'; import { RemoveLifecyclePolicyConfirmModal } from './components/remove_lifecycle_confirm_modal'; +import { Index } from '../application/services/policies/types'; const stepPath = 'ilm.step'; -export const retryLifecycleActionExtension = ({ indices }) => { +export const retryLifecycleActionExtension = ({ indices }: { indices: Index[] }) => { const allHaveErrors = every(indices, (index) => { return index.ilm && index.ilm.failed_step; }); if (!allHaveErrors) { return null; } - const indexNames = indices.map(({ name }) => name); + const indexNames = indices.map(({ name }: Index) => name); return { requestMethod: retryLifecycleForIndex, icon: 'play', @@ -35,22 +40,28 @@ export const retryLifecycleActionExtension = ({ indices }) => { 'xpack.indexLifecycleMgmt.retryIndexLifecycleAction.retriedLifecycleMessage', { defaultMessage: 'Called retry lifecycle step for: {indexNames}', - values: { indexNames: indexNames.map((indexName) => `"${indexName}"`).join(', ') }, + values: { indexNames: indexNames.map((indexName: string) => `"${indexName}"`).join(', ') }, } ), }; }; -export const removeLifecyclePolicyActionExtension = ({ indices, reloadIndices }) => { +export const removeLifecyclePolicyActionExtension = ({ + indices, + reloadIndices, +}: { + indices: Index[]; + reloadIndices: () => void; +}) => { const allHaveIlm = every(indices, (index) => { return index.ilm && index.ilm.managed; }); if (!allHaveIlm) { return null; } - const indexNames = indices.map(({ name }) => name); + const indexNames = indices.map(({ name }: Index) => name); return { - renderConfirmModal: (closeModal) => { + renderConfirmModal: (closeModal: () => void) => { return ( { +export const addLifecyclePolicyActionExtension = ({ + indices, + reloadIndices, + getUrlForApp, +}: { + indices: Index[]; + reloadIndices: () => void; + getUrlForApp: ApplicationStart['getUrlForApp']; +}) => { if (indices.length !== 1) { return null; } @@ -79,7 +98,7 @@ export const addLifecyclePolicyActionExtension = ({ indices, reloadIndices, getU } const indexName = index.name; return { - renderConfirmModal: (closeModal) => { + renderConfirmModal: (closeModal: () => void) => { return ( { +export const ilmBannerExtension = (indices: Index[]) => { const { Query } = EuiSearchBar; if (!indices.length) { return null; } - const indicesWithLifecycleErrors = indices.filter((index) => { + const indicesWithLifecycleErrors = indices.filter((index: Index) => { return get(index, stepPath) === 'ERROR'; }); const numIndicesWithLifecycleErrors = indicesWithLifecycleErrors.length; @@ -124,11 +143,14 @@ export const ilmBannerExtension = (indices) => { }; }; -export const ilmSummaryExtension = (index, getUrlForApp) => { +export const ilmSummaryExtension = ( + index: Index, + getUrlForApp: ApplicationStart['getUrlForApp'] +) => { return ; }; -export const ilmFilterExtension = (indices) => { +export const ilmFilterExtension = (indices: Index[]) => { const hasIlm = some(indices, (index) => index.ilm && index.ilm.managed); if (!hasIlm) { return []; @@ -200,7 +222,9 @@ export const ilmFilterExtension = (indices) => { } }; -export const addAllExtensions = (extensionsService) => { +export const addAllExtensions = ( + extensionsService: IndexManagementPluginSetup['extensionsService'] +) => { extensionsService.addAction(retryLifecycleActionExtension); extensionsService.addAction(removeLifecyclePolicyActionExtension); extensionsService.addAction(addLifecyclePolicyActionExtension); diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts index ecf5ba21fe60c..354e4fe67cd19 100644 --- a/x-pack/plugins/index_management/common/types/indices.ts +++ b/x-pack/plugins/index_management/common/types/indices.ts @@ -41,3 +41,18 @@ export interface IndexSettings { analysis?: AnalysisModule; [key: string]: any; } + +export interface Index { + health: string; + status: string; + name: string; + uuid: string; + primary: string; + replica: string; + documents: any; + size: any; + isFrozen: boolean; + aliases: string | string[]; + data_stream?: string; + [key: string]: any; +} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx index a50572df9004e..af0590be5c941 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -134,9 +134,9 @@ export const LoadMappingsProvider = ({ onJson, children }: Props) => { state.json !== undefined && state.errors !== undefined ? 'validationResult' : 'json'; const i18nTexts = getTexts(view, state.errors?.length); - const onJsonUpdate: OnJsonEditorUpdateHandler = (jsonUpdateData) => { + const onJsonUpdate: OnJsonEditorUpdateHandler = useCallback((jsonUpdateData) => { jsonContent.current = jsonUpdateData; - }; + }, []); const openModal: OpenJsonModalFunc = () => { setState({ isModalOpen: true }); diff --git a/x-pack/plugins/index_management/public/index.ts b/x-pack/plugins/index_management/public/index.ts index a2e9a41feb165..538dcaf25c47d 100644 --- a/x-pack/plugins/index_management/public/index.ts +++ b/x-pack/plugins/index_management/public/index.ts @@ -14,3 +14,5 @@ export const plugin = () => { export { IndexManagementPluginSetup }; export { getIndexListUri } from './application/services/routing'; + +export { Index } from '../common'; diff --git a/x-pack/plugins/index_management/server/index.ts b/x-pack/plugins/index_management/server/index.ts index 4d9409e4a516c..bf52d8a09c84c 100644 --- a/x-pack/plugins/index_management/server/index.ts +++ b/x-pack/plugins/index_management/server/index.ts @@ -18,5 +18,5 @@ export const config = { /** @public */ export { Dependencies } from './types'; export { IndexManagementPluginSetup } from './plugin'; -export { Index } from './types'; +export { Index } from '../common'; export { IndexManagementConfig } from './config'; diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.ts index ae10629e069e8..e9eaec3e22428 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.ts @@ -5,7 +5,8 @@ */ import { CatIndicesParams } from 'elasticsearch'; import { IndexDataEnricher } from '../services'; -import { Index, CallAsCurrentUser } from '../types'; +import { CallAsCurrentUser } from '../types'; +import { Index } from '../index'; interface Hit { health: string; @@ -44,7 +45,9 @@ async function fetchIndicesCall( // This call retrieves alias and settings (incl. hidden status) information about indices const indices: GetIndicesResponse = await callAsCurrentUser('transport.request', { method: 'GET', - path: `/${indexNamesString}`, + // transport.request doesn't do any URI encoding, unlike other JS client APIs. This enables + // working with Logstash indices with names like %{[@metadata][beat]}-%{[@metadata][version]}. + path: `/${encodeURIComponent(indexNamesString)}`, query: { expand_wildcards: 'hidden,all', }, diff --git a/x-pack/plugins/index_management/server/services/index_data_enricher.ts b/x-pack/plugins/index_management/server/services/index_data_enricher.ts index 7a62ce9f7a3c3..80bdf76820f28 100644 --- a/x-pack/plugins/index_management/server/services/index_data_enricher.ts +++ b/x-pack/plugins/index_management/server/services/index_data_enricher.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Index, CallAsCurrentUser } from '../types'; +import { CallAsCurrentUser } from '../types'; +import { Index } from '../index'; export type Enricher = (indices: Index[], callAsCurrentUser: CallAsCurrentUser) => Promise; diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts index cd3eb5dfecd40..fce0414dee936 100644 --- a/x-pack/plugins/index_management/server/types.ts +++ b/x-pack/plugins/index_management/server/types.ts @@ -26,19 +26,4 @@ export interface RouteDependencies { }; } -export interface Index { - health: string; - status: string; - name: string; - uuid: string; - primary: string; - replica: string; - documents: any; - size: any; - isFrozen: boolean; - aliases: string | string[]; - data_stream?: string; - [key: string]: any; -} - export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 7cd6383a9b2e5..51f91d7189db7 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -152,9 +152,8 @@ export class InfraServerPlugin { core.http.registerRouteHandlerContext( 'infra', (context, request): InfraRequestHandlerContext => { - const mlSystem = context.ml && plugins.ml?.mlSystemProvider(context.ml?.mlClient, request); - const mlAnomalyDetectors = - context.ml && plugins.ml?.anomalyDetectorsProvider(context.ml?.mlClient, request); + const mlSystem = plugins.ml?.mlSystemProvider(request); + const mlAnomalyDetectors = plugins.ml?.anomalyDetectorsProvider(request); const spaceId = plugins.spaces?.spacesService.getSpaceId(request) || 'default'; return { diff --git a/x-pack/plugins/ingest_manager/common/constants/agent.ts b/x-pack/plugins/ingest_manager/common/constants/agent.ts index 6d0d9ee801a94..82d2ad712ef02 100644 --- a/x-pack/plugins/ingest_manager/common/constants/agent.ts +++ b/x-pack/plugins/ingest_manager/common/constants/agent.ts @@ -12,6 +12,7 @@ export const AGENT_TYPE_PERMANENT = 'PERMANENT'; export const AGENT_TYPE_EPHEMERAL = 'EPHEMERAL'; export const AGENT_TYPE_TEMPORARY = 'TEMPORARY'; +export const AGENT_POLLING_REQUEST_TIMEOUT_MS = 300000; // 5 minutes export const AGENT_POLLING_THRESHOLD_MS = 30000; export const AGENT_POLLING_INTERVAL = 1000; export const AGENT_UPDATE_LAST_CHECKIN_INTERVAL_MS = 30000; diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json index 8c9ede1fde9ba..d75a914e080d7 100644 --- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json +++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json @@ -43,12 +43,9 @@ }, "perPage": { "type": "number" - }, - "success": { - "type": "boolean" } }, - "required": ["items", "total", "page", "perPage", "success"] + "required": ["items", "total", "page", "perPage"] }, "examples": { "success": { @@ -71,8 +68,7 @@ ], "total": 1, "page": 1, - "perPage": 50, - "success": true + "perPage": 50 } } } @@ -107,9 +103,6 @@ "properties": { "item": { "$ref": "#/components/schemas/AgentPolicy" - }, - "success": { - "type": "boolean" } } } @@ -159,12 +152,9 @@ "properties": { "item": { "$ref": "#/components/schemas/AgentPolicy" - }, - "success": { - "type": "boolean" } }, - "required": ["item", "success"] + "required": ["item"] }, "examples": { "success": { @@ -707,8 +697,7 @@ "revision": 2, "updated_on": "2020-05-06T17:32:21.905Z", "updated_by": "system" - }, - "success": true + } } } } @@ -733,12 +722,9 @@ "properties": { "item": { "$ref": "#/components/schemas/AgentPolicy" - }, - "success": { - "type": "boolean" } }, - "required": ["item", "success"] + "required": ["item"] }, "examples": { "example-1": { @@ -751,8 +737,7 @@ "updated_on": "Fri Feb 28 2020 16:22:31 GMT-0500 (Eastern Standard Time)", "updated_by": "elastic", "packagePolicies": [] - }, - "success": true + } } } } @@ -810,12 +795,9 @@ "properties": { "item": { "$ref": "#/components/schemas/AgentPolicy" - }, - "success": { - "type": "boolean" } }, - "required": ["item", "success"] + "required": ["item"] } } } @@ -948,12 +930,9 @@ }, "perPage": { "type": "number" - }, - "success": { - "type": "boolean" } }, - "required": ["items", "success"] + "required": ["items"] }, "examples": { "example-1": { @@ -1157,8 +1136,7 @@ ], "total": 6, "page": 1, - "perPage": 20, - "success": true + "perPage": 20 } } } @@ -1251,12 +1229,9 @@ "properties": { "item": { "$ref": "#/components/schemas/PackagePolicy" - }, - "success": { - "type": "boolean" } }, - "required": ["item", "success"] + "required": ["item"] } } } @@ -1415,9 +1390,6 @@ "properties": { "response": { "$ref": "#/components/schemas/PackageInfo" - }, - "success": { - "type": "boolean" } } }, @@ -1709,8 +1681,7 @@ }, "references": [] } - }, - "success": true + } } }, "required-package": { @@ -1899,8 +1870,7 @@ }, "references": [] } - }, - "success": true + } } } } @@ -1950,12 +1920,9 @@ }, "required": ["id", "type"] } - }, - "success": { - "type": "boolean" } }, - "required": ["response", "success"] + "required": ["response"] } } } @@ -1994,12 +1961,9 @@ }, "required": ["id", "type"] } - }, - "success": { - "type": "boolean" } }, - "required": ["response", "success"] + "required": ["response"] } } } @@ -2678,8 +2642,7 @@ "version": "1.0.0", "status": "not_installed" } - ], - "success": true + ] } } } @@ -2743,9 +2706,6 @@ "type": "object" } }, - "success": { - "type": "boolean" - }, "total": { "type": "number" }, @@ -2756,7 +2716,7 @@ "type": "number" } }, - "required": ["list", "success", "total", "page", "perPage"] + "required": ["list", "total", "page", "perPage"] }, "examples": { "example-1": { @@ -2793,7 +2753,6 @@ "status": "online" } ], - "success": true, "total": 1, "page": 1, "perPage": 20 @@ -2836,12 +2795,9 @@ "properties": { "item": { "type": "object" - }, - "success": { - "type": "string" } }, - "required": ["item", "success"] + "required": ["item"] } } } @@ -2916,9 +2872,6 @@ "type": "string", "enum": ["checkin"] }, - "success": { - "type": "string" - }, "actions": { "type": "array", "items": { @@ -2950,7 +2903,6 @@ "success": { "value": { "action": "checkin", - "success": true, "actions": [ { "agent_id": "a6f14bd2-1a2a-481c-9212-9494d064ffdf", @@ -3346,21 +3298,17 @@ "schema": { "type": "object", "properties": { - "success": { - "type": "boolean" - }, "action": { "type": "string", "enum": ["acks"] } }, - "required": ["success", "action"] + "required": ["action"] }, "examples": { "success": { "value": { - "action": "checkin", - "success": true + "action": "checkin" } } } @@ -3417,9 +3365,6 @@ "action": { "type": "string" }, - "success": { - "type": "boolean" - }, "item": { "$ref": "#/components/schemas/Agent" } @@ -3429,7 +3374,6 @@ "success": { "value": { "action": "created", - "success": true, "item": { "id": "8086fb1a-72ca-4a67-8533-09300c1639fa", "active": true, diff --git a/x-pack/plugins/ingest_manager/common/services/index.ts b/x-pack/plugins/ingest_manager/common/services/index.ts index ad739bf9ff844..46a1c65872d1b 100644 --- a/x-pack/plugins/ingest_manager/common/services/index.ts +++ b/x-pack/plugins/ingest_manager/common/services/index.ts @@ -11,3 +11,4 @@ export { fullAgentPolicyToYaml } from './full_agent_policy_to_yaml'; export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from './limited_package'; export { decodeCloudId } from './decode_cloud_id'; export { isValidNamespace } from './is_valid_namespace'; +export { isDiffPathProtocol } from './is_diff_path_protocol'; diff --git a/x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.test.ts b/x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.test.ts new file mode 100644 index 0000000000000..c488d552d7676 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { isDiffPathProtocol } from './is_diff_path_protocol'; + +describe('Ingest Manager - isDiffPathProtocol', () => { + it('returns true for different paths', () => { + expect( + isDiffPathProtocol([ + 'http://localhost:8888/abc', + 'http://localhost:8888/abc', + 'http://localhost:8888/efg', + ]) + ).toBe(true); + }); + it('returns true for different protocols', () => { + expect( + isDiffPathProtocol([ + 'http://localhost:8888/abc', + 'https://localhost:8888/abc', + 'http://localhost:8888/abc', + ]) + ).toBe(true); + }); + it('returns false for same paths and protocols and different host or port', () => { + expect( + isDiffPathProtocol([ + 'http://localhost:8888/abc', + 'http://localhost2:8888/abc', + 'http://localhost:8883/abc', + ]) + ).toBe(false); + }); + it('returns false for one url', () => { + expect(isDiffPathProtocol(['http://localhost:8888/abc'])).toBe(false); + }); +}); diff --git a/x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.ts b/x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.ts new file mode 100644 index 0000000000000..666e886d745b1 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/is_diff_path_protocol.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. + */ + +/* + * 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. + */ + +// validates an array of urls have the same path and protocol +export function isDiffPathProtocol(kibanaUrls: string[]) { + const urlCompare = new URL(kibanaUrls[0]); + const compareProtocol = urlCompare.protocol; + const comparePathname = urlCompare.pathname; + return kibanaUrls.some((v) => { + const url = new URL(v); + const protocol = url.protocol; + const pathname = url.pathname; + return compareProtocol !== protocol || comparePathname !== pathname; + }); +} diff --git a/x-pack/plugins/ingest_manager/common/services/package_policies_to_agent_inputs.test.ts b/x-pack/plugins/ingest_manager/common/services/package_policies_to_agent_inputs.test.ts index 956423a497373..1df06df1de275 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_policies_to_agent_inputs.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_policies_to_agent_inputs.test.ts @@ -117,6 +117,7 @@ describe('Ingest Manager - storedPackagePoliciesToAgentInputs', () => { { id: 'some-uuid', name: 'mock-package-policy', + revision: 1, type: 'test-logs', data_stream: { namespace: 'default' }, use_output: 'default', @@ -159,6 +160,7 @@ describe('Ingest Manager - storedPackagePoliciesToAgentInputs', () => { { id: 'some-uuid', name: 'mock-package-policy', + revision: 1, type: 'test-logs', data_stream: { namespace: 'default' }, use_output: 'default', diff --git a/x-pack/plugins/ingest_manager/common/services/package_policies_to_agent_inputs.ts b/x-pack/plugins/ingest_manager/common/services/package_policies_to_agent_inputs.ts index 639f20eb08828..e74256ce732a6 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_policies_to_agent_inputs.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_policies_to_agent_inputs.ts @@ -22,6 +22,7 @@ export const storedPackagePoliciesToAgentInputs = ( const fullInput: FullAgentPolicyInput = { id: packagePolicy.id || packagePolicy.name, + revision: packagePolicy.revision, name: packagePolicy.name, type: input.type, data_stream: { diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index cafd0f03f66e2..d62f4fbb023dc 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -15,7 +15,7 @@ export interface IngestManagerConfigType { pollingRequestTimeout: number; maxConcurrentConnections: number; kibana: { - host?: string; + host?: string[] | string; ca_sha256?: string; }; elasticsearch: { diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts index c626c85d3fb24..b3b3004f4fc5d 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts @@ -42,6 +42,7 @@ export interface FullAgentPolicyInputStream { export interface FullAgentPolicyInput { id: string; name: string; + revision: number; type: string; data_stream: { namespace: string }; use_output: string; @@ -60,6 +61,11 @@ export interface FullAgentPolicy { [key: string]: any; }; }; + fleet?: { + kibana: { + hosts: string[]; + }; + }; inputs: FullAgentPolicyInput[]; revision?: number; agent?: { diff --git a/x-pack/plugins/ingest_manager/common/types/models/settings.ts b/x-pack/plugins/ingest_manager/common/types/models/settings.ts index 98d99911f1b3f..f554f4b392ad6 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/settings.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/settings.ts @@ -5,10 +5,10 @@ */ import { SavedObjectAttributes } from 'src/core/public'; -interface BaseSettings { - agent_auto_upgrade?: boolean; - package_auto_upgrade?: boolean; - kibana_url?: string; +export interface BaseSettings { + agent_auto_upgrade: boolean; + package_auto_upgrade: boolean; + kibana_urls: string[]; kibana_ca_sha256?: string; has_seen_add_data_notice?: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index 4a50938049a7b..cf8d3ab1c908a 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -28,7 +28,6 @@ export interface GetAgentsResponse { total: number; page: number; perPage: number; - success: boolean; } export interface GetOneAgentRequest { @@ -39,7 +38,6 @@ export interface GetOneAgentRequest { export interface GetOneAgentResponse { item: Agent; - success: boolean; } export interface PostAgentCheckinRequest { @@ -55,7 +53,7 @@ export interface PostAgentCheckinRequest { export interface PostAgentCheckinResponse { action: string; - success: boolean; + actions: AgentAction[]; } @@ -72,7 +70,7 @@ export interface PostAgentEnrollRequest { export interface PostAgentEnrollResponse { action: string; - success: boolean; + item: Agent & { status: AgentStatus }; } @@ -87,7 +85,6 @@ export interface PostAgentAcksRequest { export interface PostAgentAcksResponse { action: string; - success: boolean; } export interface PostNewAgentActionRequest { @@ -100,7 +97,6 @@ export interface PostNewAgentActionRequest { } export interface PostNewAgentActionResponse { - success: boolean; item: AgentAction; } @@ -110,9 +106,8 @@ export interface PostAgentUnenrollRequest { }; } -export interface PostAgentUnenrollResponse { - success: boolean; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PostAgentUnenrollResponse {} export interface PutAgentReassignRequest { params: { @@ -121,9 +116,8 @@ export interface PutAgentReassignRequest { body: { policy_id: string }; } -export interface PutAgentReassignResponse { - success: boolean; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PutAgentReassignResponse {} export interface GetOneAgentEventsRequest { params: { @@ -141,7 +135,6 @@ export interface GetOneAgentEventsResponse { total: number; page: number; perPage: number; - success: boolean; } export interface DeleteAgentRequest { @@ -166,7 +159,6 @@ export interface GetAgentStatusRequest { } export interface GetAgentStatusResponse { - success: boolean; results: { events: number; total: number; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts index 4683727ed51ac..aa9fbc20fc0b0 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_policy.ts @@ -19,7 +19,6 @@ export interface GetAgentPoliciesResponse { total: number; page: number; perPage: number; - success: boolean; } export interface GetOneAgentPolicyRequest { @@ -30,7 +29,6 @@ export interface GetOneAgentPolicyRequest { export interface GetOneAgentPolicyResponse { item: AgentPolicy; - success: boolean; } export interface CreateAgentPolicyRequest { @@ -39,7 +37,6 @@ export interface CreateAgentPolicyRequest { export interface CreateAgentPolicyResponse { item: AgentPolicy; - success: boolean; } export type UpdateAgentPolicyRequest = GetOneAgentPolicyRequest & { @@ -48,7 +45,6 @@ export type UpdateAgentPolicyRequest = GetOneAgentPolicyRequest & { export interface UpdateAgentPolicyResponse { item: AgentPolicy; - success: boolean; } export interface CopyAgentPolicyRequest { @@ -57,7 +53,6 @@ export interface CopyAgentPolicyRequest { export interface CopyAgentPolicyResponse { item: AgentPolicy; - success: boolean; } export interface DeleteAgentPolicyRequest { @@ -68,7 +63,6 @@ export interface DeleteAgentPolicyRequest { export interface DeleteAgentPolicyResponse { id: string; - success: boolean; } export interface GetFullAgentPolicyRequest { @@ -79,5 +73,4 @@ export interface GetFullAgentPolicyRequest { export interface GetFullAgentPolicyResponse { item: FullAgentPolicy; - success: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/enrollment_api_key.ts index 1929955cac84c..1d1c2f79bf6cb 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/enrollment_api_key.ts @@ -19,7 +19,6 @@ export interface GetEnrollmentAPIKeysResponse { total: number; page: number; perPage: number; - success: boolean; } export interface GetOneEnrollmentAPIKeyRequest { @@ -30,7 +29,6 @@ export interface GetOneEnrollmentAPIKeyRequest { export interface GetOneEnrollmentAPIKeyResponse { item: EnrollmentAPIKey; - success: boolean; } export interface DeleteEnrollmentAPIKeyRequest { @@ -41,7 +39,6 @@ export interface DeleteEnrollmentAPIKeyRequest { export interface DeleteEnrollmentAPIKeyResponse { action: string; - success: boolean; } export interface PostEnrollmentAPIKeyRequest { @@ -55,5 +52,4 @@ export interface PostEnrollmentAPIKeyRequest { export interface PostEnrollmentAPIKeyResponse { action: string; item: EnrollmentAPIKey; - success: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts index 1901b8c0c7039..5fb718f91b876 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts @@ -20,7 +20,6 @@ export interface GetCategoriesRequest { export interface GetCategoriesResponse { response: CategorySummaryList; - success: boolean; } export interface GetPackagesRequest { @@ -39,12 +38,10 @@ export interface GetPackagesResponse { > > >; - success: boolean; } export interface GetLimitedPackagesResponse { response: string[]; - success: boolean; } export interface GetFileRequest { @@ -62,7 +59,6 @@ export interface GetInfoRequest { export interface GetInfoResponse { response: PackageInfo; - success: boolean; } export interface InstallPackageRequest { @@ -73,7 +69,6 @@ export interface InstallPackageRequest { export interface InstallPackageResponse { response: AssetReference[]; - success: boolean; } export interface DeletePackageRequest { @@ -84,5 +79,4 @@ export interface DeletePackageRequest { export interface DeletePackageResponse { response: AssetReference[]; - success: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts index 4162060363381..87e8a0977e3ba 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts @@ -7,7 +7,6 @@ import { Output } from '../models'; export interface GetOneOutputResponse { item: Output; - success: boolean; } export interface GetOneOutputRequest { @@ -28,7 +27,6 @@ export interface PutOutputRequest { export interface PutOutputResponse { item: Output; - success: boolean; } export interface GetOutputsResponse { @@ -36,5 +34,4 @@ export interface GetOutputsResponse { total: number; page: number; perPage: number; - success: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts index e5bba3d6deab3..61669ab876d80 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts @@ -18,7 +18,6 @@ export interface GetPackagePoliciesResponse { total: number; page: number; perPage: number; - success: boolean; } export interface GetOnePackagePolicyRequest { @@ -29,7 +28,6 @@ export interface GetOnePackagePolicyRequest { export interface GetOnePackagePolicyResponse { item: PackagePolicy; - success: boolean; } export interface CreatePackagePolicyRequest { @@ -38,7 +36,6 @@ export interface CreatePackagePolicyRequest { export interface CreatePackagePolicyResponse { item: PackagePolicy; - success: boolean; } export type UpdatePackagePolicyRequest = GetOnePackagePolicyRequest & { diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts index c02a5e5878ee9..90d623b8a4be9 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts @@ -7,7 +7,6 @@ import { Settings } from '../models'; export interface GetSettingsResponse { item: Settings; - success: boolean; } export interface PutSettingsRequest { @@ -16,5 +15,4 @@ export interface PutSettingsRequest { export interface PutSettingsResponse { item: Settings; - success: boolean; } diff --git a/x-pack/plugins/ingest_manager/package.json b/x-pack/plugins/ingest_manager/package.json index 8826ed57ab106..871972729b9f3 100644 --- a/x-pack/plugins/ingest_manager/package.json +++ b/x-pack/plugins/ingest_manager/package.json @@ -5,6 +5,7 @@ "private": true, "license": "Elastic-License", "dependencies": { - "abort-controller": "^3.0.0" + "abort-controller": "^3.0.0", + "ajv": "^6.12.4" } } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx index 9a9557f77c40c..e0d843ad773b8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx @@ -18,14 +18,14 @@ import { EuiFlyoutFooter, EuiForm, EuiFormRow, - EuiFieldText, EuiRadioGroup, EuiComboBox, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText } from '@elastic/eui'; -import { useInput, useComboInput, useCore, useGetSettings, sendPutSettings } from '../hooks'; +import { useComboInput, useCore, useGetSettings, sendPutSettings } from '../hooks'; import { useGetOutputs, sendPutOutput } from '../hooks/use_request/outputs'; +import { isDiffPathProtocol } from '../../../../common/'; const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm; @@ -36,14 +36,28 @@ interface Props { function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { const [isLoading, setIsloading] = React.useState(false); const { notifications } = useCore(); - const kibanaUrlInput = useInput('', (value) => { - if (!value.match(URL_REGEX)) { + const kibanaUrlsInput = useComboInput([], (value) => { + if (value.length === 0) { + return [ + i18n.translate('xpack.ingestManager.settings.kibanaUrlEmptyError', { + defaultMessage: 'At least one URL is required', + }), + ]; + } + if (value.some((v) => !v.match(URL_REGEX))) { return [ i18n.translate('xpack.ingestManager.settings.kibanaUrlError', { defaultMessage: 'Invalid URL', }), ]; } + if (isDiffPathProtocol(value)) { + return [ + i18n.translate('xpack.ingestManager.settings.kibanaUrlDifferentPathOrProtocolError', { + defaultMessage: 'Protocol and path must be the same for each URL', + }), + ]; + } }); const elasticsearchUrlInput = useComboInput([], (value) => { if (value.some((v) => !v.match(URL_REGEX))) { @@ -58,7 +72,7 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { return { isLoading, onSubmit: async () => { - if (!kibanaUrlInput.validate() || !elasticsearchUrlInput.validate()) { + if (!kibanaUrlsInput.validate() || !elasticsearchUrlInput.validate()) { return; } @@ -74,7 +88,7 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { throw outputResponse.error; } const settingsResponse = await sendPutSettings({ - kibana_url: kibanaUrlInput.value, + kibana_urls: kibanaUrlsInput.value, }); if (settingsResponse.error) { throw settingsResponse.error; @@ -94,14 +108,13 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) { } }, inputs: { - kibanaUrl: kibanaUrlInput, + kibanaUrls: kibanaUrlsInput, elasticsearchUrl: elasticsearchUrlInput, }, }; } export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { - const core = useCore(); const settingsRequest = useGetSettings(); const settings = settingsRequest?.data?.item; const outputsRequest = useGetOutputs(); @@ -117,9 +130,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { useEffect(() => { if (settings) { - inputs.kibanaUrl.setValue( - settings.kibana_url || `${window.location.origin}${core.http.basePath.get()}` - ); + inputs.kibanaUrls.setValue(settings.kibana_urls); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [settings]); @@ -220,9 +231,9 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { label={i18n.translate('xpack.ingestManager.settings.kibanaUrlLabel', { defaultMessage: 'Kibana URL', })} - {...inputs.kibanaUrl.formRowProps} + {...inputs.kibanaUrls.formRowProps} > - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_copy_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_copy_provider.tsx index 8a91cabe78d00..fefbe1fa82a0c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_copy_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_copy_provider.tsx @@ -61,7 +61,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr try { const { data } = await sendCopyAgentPolicy(agentPolicy!.id, newAgentPolicy!); - if (data?.success) { + if (data) { notifications.toasts.addSuccess( i18n.translate('xpack.ingestManager.copyAgentPolicy.successNotificationTitle', { defaultMessage: 'Agent policy copied', @@ -70,9 +70,7 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr if (onSuccessCallback.current) { onSuccessCallback.current(data.item); } - } - - if (!data?.success) { + } else { notifications.toasts.addDanger( i18n.translate('xpack.ingestManager.copyAgentPolicy.failureNotificationTitle', { defaultMessage: "Error copying agent policy '{id}'", diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx index 08367bf2a97bf..15f71689ecdfa 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_delete_provider.tsx @@ -59,7 +59,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil agentPolicyId: agentPolicy!, }); - if (data?.success) { + if (data) { notifications.toasts.addSuccess( i18n.translate('xpack.ingestManager.deleteAgentPolicy.successSingleNotificationTitle', { defaultMessage: "Deleted agent policy '{id}'", @@ -69,9 +69,7 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil if (onSuccessCallback.current) { onSuccessCallback.current(agentPolicy!); } - } - - if (!data?.success) { + } else { notifications.toasts.addDanger( i18n.translate('xpack.ingestManager.deleteAgentPolicy.failureSingleNotificationTitle', { defaultMessage: "Error deleting agent policy '{id}'", diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_yaml_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_yaml_flyout.tsx index 919bb49f69aae..5d485a6e21086 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_yaml_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_yaml_flyout.tsx @@ -18,6 +18,7 @@ import { EuiFlyoutFooter, EuiButtonEmpty, EuiButton, + EuiCallOut, } from '@elastic/eui'; import { useGetOneAgentPolicyFull, useGetOneAgentPolicy, useCore } from '../../../hooks'; import { Loading } from '../../../components'; @@ -32,17 +33,28 @@ const FlyoutBody = styled(EuiFlyoutBody)` export const AgentPolicyYamlFlyout = memo<{ policyId: string; onClose: () => void }>( ({ policyId, onClose }) => { const core = useCore(); - const { isLoading: isLoadingYaml, data: yamlData } = useGetOneAgentPolicyFull(policyId); + const { isLoading: isLoadingYaml, data: yamlData, error } = useGetOneAgentPolicyFull(policyId); const { data: agentPolicyData } = useGetOneAgentPolicy(policyId); - - const body = - isLoadingYaml && !yamlData ? ( - - ) : ( - - {fullAgentPolicyToYaml(yamlData!.item)} - - ); + const body = isLoadingYaml ? ( + + ) : error ? ( + + } + color="danger" + iconType="alert" + > + {error.message} + + ) : ( + + {fullAgentPolicyToYaml(yamlData!.item)} + + ); const downloadLink = core.http.basePath.prepend( agentPolicyRouteService.getInfoFullDownloadPath(policyId) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/settings/index.tsx index 766bdc2e2c193..b8fbb4f1835a6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/settings/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/components/settings/index.tsx @@ -82,7 +82,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( namespace, monitoring_enabled, }); - if (data?.success) { + if (data) { notifications.toasts.addSuccess( i18n.translate('xpack.ingestManager.editAgentPolicy.successNotificationTitle', { defaultMessage: "Successfully updated '{name}' settings", diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx index b072990727e3c..e3e2975777e17 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx @@ -116,7 +116,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({ try { const { data, error } = await createAgentPolicy(); setIsLoading(false); - if (data?.success) { + if (data) { notifications.toasts.addSuccess( i18n.translate( 'xpack.ingestManager.createAgentPolicy.successNotificationTitle', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx index 414c4e0d7ad2c..05817f28f9292 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx @@ -8,6 +8,8 @@ import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, + EuiSpacer, + EuiText, EuiTitle, EuiFlexGroup, EuiFlexItem, @@ -44,6 +46,14 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ />

    + + + + + setMode('managed')}> = ({ agentPolic const settings = useGetSettings(); const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId); - const kibanaUrl = - settings.data?.item?.kibana_url ?? `${window.location.origin}${core.http.basePath.get()}`; + const kibanaUrlsSettings = settings.data?.item?.kibana_urls; + const kibanaUrl = kibanaUrlsSettings + ? kibanaUrlsSettings[0] + : `${window.location.origin}${core.http.basePath.get()}`; + const kibanaCASha256 = settings.data?.item?.kibana_ca_sha256; const steps: EuiContainedStepProps[] = [ @@ -72,13 +75,13 @@ export const ManagedInstructions: React.FunctionComponent = ({ agentPolic <>
    ), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx index 049ceca82b309..9262cc2cb42ac 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -15,12 +15,13 @@ import { EuiFlexGroup, EuiCodeBlock, EuiCopy, + EuiLink, } from '@elastic/eui'; import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentPolicy } from '../../../../types'; -import { useCore, sendGetOneAgentPolicyFull } from '../../../../hooks'; +import { useCore, useLink, sendGetOneAgentPolicyFull } from '../../../../hooks'; import { DownloadStep, AgentPolicySelectionStep } from './steps'; import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../../../services'; @@ -31,6 +32,7 @@ interface Props { const RUN_INSTRUCTIONS = './elastic-agent run'; export const StandaloneInstructions: React.FunctionComponent = ({ agentPolicies }) => { + const { getHref } = useLink(); const core = useCore(); const { notifications } = core; @@ -157,7 +159,17 @@ export const StandaloneInstructions: React.FunctionComponent = ({ agentPo + + + ), + }} /> diff --git a/x-pack/plugins/ingest_manager/scripts/dev_agent/script.ts b/x-pack/plugins/ingest_manager/scripts/dev_agent/script.ts index c7b8edd0c332e..65375a076e9a4 100644 --- a/x-pack/plugins/ingest_manager/scripts/dev_agent/script.ts +++ b/x-pack/plugins/ingest_manager/scripts/dev_agent/script.ts @@ -122,7 +122,7 @@ async function enroll(kibanaURL: string, apiKey: string, log: ToolingLog): Promi }); const obj: PostAgentEnrollResponse = await res.json(); - if (!obj.success) { + if (!res.ok) { log.error(JSON.stringify(obj, null, 2)); throw new Error('unable to enroll'); } diff --git a/x-pack/plugins/ingest_manager/server/errors.test.ts b/x-pack/plugins/ingest_manager/server/errors.test.ts new file mode 100644 index 0000000000000..70e3a3b4150ad --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/errors.test.ts @@ -0,0 +1,191 @@ +/* + * 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 { httpServerMock } from 'src/core/server/mocks'; +import { createAppContextStartContractMock } from './mocks'; + +import { + IngestManagerError, + RegistryError, + PackageNotFoundError, + defaultIngestErrorHandler, +} from './errors'; +import { appContextService } from './services'; + +describe('defaultIngestErrorHandler', () => { + let mockContract: ReturnType; + beforeEach(async () => { + // prevents `Logger not set.` and other appContext errors + mockContract = createAppContextStartContractMock(); + appContextService.start(mockContract); + }); + + afterEach(async () => { + jest.clearAllMocks(); + appContextService.stop(); + }); + + describe('IngestManagerError', () => { + it('502: RegistryError', async () => { + const error = new RegistryError('xyz'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 502, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith(error.message); + }); + + it('404: PackageNotFoundError', async () => { + const error = new PackageNotFoundError('123'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 404, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith(error.message); + }); + + it('400: IngestManagerError', async () => { + const error = new IngestManagerError('123'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 400, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith(error.message); + }); + }); + + describe('Boom', () => { + it('500: constructor - one arg', async () => { + const error = new Boom('bam'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 500, + body: { message: 'An internal server error occurred' }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith('An internal server error occurred'); + }); + + it('custom: constructor - 2 args', async () => { + const error = new Boom('Problem doing something', { + statusCode: 456, + }); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 456, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith('Problem doing something'); + }); + + it('400: Boom.badRequest', async () => { + const error = Boom.badRequest('nope'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 400, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith('nope'); + }); + + it('404: Boom.notFound', async () => { + const error = Boom.notFound('sorry'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 404, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith('sorry'); + }); + }); + + describe('all other errors', () => { + it('500', async () => { + const error = new Error('something'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 500, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith(error); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/errors.ts b/x-pack/plugins/ingest_manager/server/errors.ts index 401211409ebf7..9829a4de23d7b 100644 --- a/x-pack/plugins/ingest_manager/server/errors.ts +++ b/x-pack/plugins/ingest_manager/server/errors.ts @@ -5,6 +5,26 @@ */ /* eslint-disable max-classes-per-file */ +import Boom, { isBoom } from 'boom'; +import { + RequestHandlerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, +} from 'src/core/server'; +import { appContextService } from './services'; + +type IngestErrorHandler = ( + params: IngestErrorHandlerParams +) => IKibanaResponse | Promise; + +interface IngestErrorHandlerParams { + error: IngestManagerError | Boom | Error; + response: KibanaResponseFactory; + request?: KibanaRequest; + context?: RequestHandlerContext; +} + export class IngestManagerError extends Error { constructor(message?: string) { super(message); @@ -12,20 +32,53 @@ export class IngestManagerError extends Error { } } -export const getHTTPResponseCode = (error: IngestManagerError): number => { +const getHTTPResponseCode = (error: IngestManagerError): number => { if (error instanceof RegistryError) { return 502; // Bad Gateway } if (error instanceof PackageNotFoundError) { - return 404; + return 404; // Not Found } - if (error instanceof PackageOutdatedError) { - return 400; - } else { - return 400; // Bad Request + + return 400; // Bad Request +}; + +export const defaultIngestErrorHandler: IngestErrorHandler = async ({ + error, + response, +}: IngestErrorHandlerParams): Promise => { + const logger = appContextService.getLogger(); + + // our "expected" errors + if (error instanceof IngestManagerError) { + // only log the message + logger.error(error.message); + return response.customError({ + statusCode: getHTTPResponseCode(error), + body: { message: error.message }, + }); } + + // handle any older Boom-based errors or the few places our app uses them + if (isBoom(error)) { + // only log the message + logger.error(error.output.payload.message); + return response.customError({ + statusCode: error.output.statusCode, + body: { message: error.output.payload.message }, + }); + } + + // not sure what type of error this is. log as much as possible + logger.error(error); + return response.customError({ + statusCode: 500, + body: { message: error.message }, + }); }; export class RegistryError extends IngestManagerError {} +export class RegistryConnectionError extends RegistryError {} +export class RegistryResponseError extends RegistryError {} export class PackageNotFoundError extends IngestManagerError {} export class PackageOutdatedError extends IngestManagerError {} diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 962cddb2e411e..e8ea886cbf9f5 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -9,6 +9,7 @@ import { IngestManagerPlugin } from './plugin'; import { AGENT_POLICY_ROLLOUT_RATE_LIMIT_INTERVAL_MS, AGENT_POLICY_ROLLOUT_RATE_LIMIT_REQUEST_PER_INTERVAL, + AGENT_POLLING_REQUEST_TIMEOUT_MS, } from '../common'; export { AgentService, ESIndexPatternService, getRegistryUrl } from './services'; export { @@ -29,10 +30,18 @@ export const config = { fleet: schema.object({ enabled: schema.boolean({ defaultValue: true }), tlsCheckDisabled: schema.boolean({ defaultValue: false }), - pollingRequestTimeout: schema.number({ defaultValue: 60000 }), + pollingRequestTimeout: schema.number({ + defaultValue: AGENT_POLLING_REQUEST_TIMEOUT_MS, + min: 5000, + }), maxConcurrentConnections: schema.number({ defaultValue: 0 }), kibana: schema.object({ - host: schema.maybe(schema.string()), + host: schema.maybe( + schema.oneOf([ + schema.uri({ scheme: ['http', 'https'] }), + schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }), { minSize: 1 }), + ]) + ), ca_sha256: schema.maybe(schema.string()), }), elasticsearch: schema.object({ diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 4321dca7102a1..4a7677d69d6e7 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -238,7 +238,7 @@ export class IngestManagerPlugin // we currently only use this global interceptor if fleet is enabled // since it would run this func on *every* req (other plugins, CSS, etc) registerLimitedConcurrencyRoutes(core, config); - registerAgentRoutes(router); + registerAgentRoutes(router, config); registerEnrollmentApiKeyRoutes(router); registerInstallScriptRoutes({ router, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts index aaed189ae3ddd..33b9dc617075b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts @@ -88,7 +88,6 @@ describe('test acks handlers', () => { await postAgentAcksHandler(({} as unknown) as RequestHandlerContext, mockRequest, mockResponse); expect(mockResponse.ok.mock.calls[0][0]?.body as PostAgentAcksResponse).toEqual({ action: 'acks', - success: true, }); }); }); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts index 0b719d8a67df7..b0439b30e8973 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts @@ -7,19 +7,13 @@ // handlers that handle events from agents in response to actions received import { RequestHandler } from 'kibana/server'; -import { TypeOf } from '@kbn/config-schema'; -import { PostAgentAcksRequestSchema } from '../../types/rest_spec'; import { AcksService } from '../../services/agents'; import { AgentEvent } from '../../../common/types/models'; -import { PostAgentAcksResponse } from '../../../common/types/rest_spec'; +import { PostAgentAcksRequest, PostAgentAcksResponse } from '../../../common/types/rest_spec'; export const postAgentAcksHandlerBuilder = function ( ackService: AcksService -): RequestHandler< - TypeOf, - undefined, - TypeOf -> { +): RequestHandler { return async (context, request, response) => { try { const soClient = ackService.getSavedObjectsClientContract(request); @@ -46,7 +40,6 @@ export const postAgentAcksHandlerBuilder = function ( const body: PostAgentAcksResponse = { action: 'acks', - success: true, }; return response.ok({ body }); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts index bcb9a7797f26a..5445a46fbe2b4 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts @@ -97,6 +97,5 @@ describe('test actions handlers', () => { ?.body as unknown) as PostNewAgentActionResponse; expect(expectedAgentActionResponse.item).toEqual(agentAction); - expect(expectedAgentActionResponse.success).toEqual(true); }); }); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts index 81893b1e78338..b81d44c40f8eb 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts @@ -35,7 +35,6 @@ export const postNewAgentActionHandlerBuilder = function ( }); const body: PostNewAgentActionResponse = { - success: true, item: savedAgentAction, }; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 3e80a74dc2b11..605e4db230ce5 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -15,6 +15,7 @@ import { PostAgentEnrollResponse, GetAgentStatusResponse, PutAgentReassignResponse, + PostAgentEnrollRequest, } from '../../../common/types'; import { GetAgentsRequestSchema, @@ -22,8 +23,7 @@ import { UpdateAgentRequestSchema, DeleteAgentRequestSchema, GetOneAgentEventsRequestSchema, - PostAgentCheckinRequestSchema, - PostAgentEnrollRequestSchema, + PostAgentCheckinRequest, GetAgentStatusRequestSchema, PutAgentReassignRequestSchema, } from '../../types'; @@ -43,7 +43,6 @@ export const getAgentHandler: RequestHandler, + PostAgentCheckinRequest['params'], undefined, - TypeOf + PostAgentCheckinRequest['body'] > = async (context, request, response) => { try { const soClient = appContextService.getInternalUserSOClient(request); @@ -187,7 +183,6 @@ export const postAgentCheckinHandler: RequestHandler< ); const body: PostAgentCheckinResponse = { action: 'checkin', - success: true, actions: actions.map((a) => ({ agent_id: agent.id, type: a.type, @@ -223,7 +218,7 @@ export const postAgentCheckinHandler: RequestHandler< export const postAgentEnrollHandler: RequestHandler< undefined, undefined, - TypeOf + PostAgentEnrollRequest['body'] > = async (context, request, response) => { try { const soClient = appContextService.getInternalUserSOClient(request); @@ -248,7 +243,6 @@ export const postAgentEnrollHandler: RequestHandler< ); const body: PostAgentEnrollResponse = { action: 'created', - success: true, item: { ...agent, status: AgentService.getAgentStatus(agent), @@ -289,7 +283,6 @@ export const getAgentsHandler: RequestHandler< ...agent, status: AgentService.getAgentStatus(agent), })), - success: true, total, page, perPage, @@ -312,9 +305,7 @@ export const putAgentsReassignHandler: RequestHandler< try { await AgentService.reassignAgent(soClient, request.params.agentId, request.body.policy_id); - const body: PutAgentReassignResponse = { - success: true, - }; + const body: PutAgentReassignResponse = {}; return response.ok({ body }); } catch (e) { return response.customError({ @@ -336,7 +327,7 @@ export const getAgentStatusForAgentPolicyHandler: RequestHandler< request.query.policyId ); - const body: GetAgentStatusResponse = { results, success: true }; + const body: GetAgentStatusResponse = { results }; return response.ok({ body }); } catch (e) { diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index 7c98ad31e5cdb..a2e5c742ad6b5 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -9,7 +9,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from 'src/core/server'; +import { IRouter, RouteValidationResultFactory } from 'src/core/server'; +import Ajv from 'ajv'; import { PLUGIN_ID, AGENT_API_ROUTES, LIMITED_CONCURRENCY_ROUTE_TAG } from '../../constants'; import { GetAgentsRequestSchema, @@ -17,13 +18,15 @@ import { GetOneAgentEventsRequestSchema, UpdateAgentRequestSchema, DeleteAgentRequestSchema, - PostAgentCheckinRequestSchema, - PostAgentEnrollRequestSchema, - PostAgentAcksRequestSchema, + PostAgentCheckinRequestBodyJSONSchema, + PostAgentCheckinRequestParamsJSONSchema, + PostAgentAcksRequestParamsJSONSchema, + PostAgentAcksRequestBodyJSONSchema, PostAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, PostNewAgentActionRequestSchema, PutAgentReassignRequestSchema, + PostAgentEnrollRequestBodyJSONSchema, } from '../../types'; import { getAgentsHandler, @@ -41,8 +44,32 @@ import * as AgentService from '../../services/agents'; import { postNewAgentActionHandlerBuilder } from './actions_handlers'; import { appContextService } from '../../services'; import { postAgentsUnenrollHandler } from './unenroll_handler'; +import { IngestManagerConfigType } from '../..'; -export const registerRoutes = (router: IRouter) => { +const ajv = new Ajv({ + coerceTypes: true, + useDefaults: true, + removeAdditional: true, + allErrors: false, + nullable: true, +}); + +function schemaErrorsText(errors: Ajv.ErrorObject[], dataVar: any) { + return errors.map((e) => `${dataVar + (e.dataPath || '')} ${e.message}`).join(', '); +} + +function makeValidator(jsonSchema: any) { + const validator = ajv.compile(jsonSchema); + return function validateWithAJV(data: any, r: RouteValidationResultFactory) { + if (validator(data)) { + return r.ok(data); + } + + return r.badRequest(schemaErrorsText(validator.errors || [], data)); + }; +} + +export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) => { // Get one router.get( { @@ -80,12 +107,25 @@ export const registerRoutes = (router: IRouter) => { getAgentsHandler ); + const pollingRequestTimeout = config.fleet.pollingRequestTimeout; // Agent checkin router.post( { path: AGENT_API_ROUTES.CHECKIN_PATTERN, - validate: PostAgentCheckinRequestSchema, - options: { tags: [] }, + validate: { + params: makeValidator(PostAgentCheckinRequestParamsJSONSchema), + body: makeValidator(PostAgentCheckinRequestBodyJSONSchema), + }, + options: { + tags: [], + ...(pollingRequestTimeout + ? { + timeout: { + idleSocket: pollingRequestTimeout, + }, + } + : {}), + }, }, postAgentCheckinHandler ); @@ -94,7 +134,9 @@ export const registerRoutes = (router: IRouter) => { router.post( { path: AGENT_API_ROUTES.ENROLL_PATTERN, - validate: PostAgentEnrollRequestSchema, + validate: { + body: makeValidator(PostAgentEnrollRequestBodyJSONSchema), + }, options: { tags: [LIMITED_CONCURRENCY_ROUTE_TAG] }, }, postAgentEnrollHandler @@ -104,7 +146,10 @@ export const registerRoutes = (router: IRouter) => { router.post( { path: AGENT_API_ROUTES.ACKS_PATTERN, - validate: PostAgentAcksRequestSchema, + validate: { + params: makeValidator(PostAgentAcksRequestParamsJSONSchema), + body: makeValidator(PostAgentAcksRequestBodyJSONSchema), + }, options: { tags: [LIMITED_CONCURRENCY_ROUTE_TAG] }, }, postAgentAcksHandlerBuilder({ diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts b/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts index d1e54fe4fb3a1..5df695d248f5b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts @@ -23,9 +23,7 @@ export const postAgentsUnenrollHandler: RequestHandler< await AgentService.unenrollAgent(soClient, request.params.agentId); } - const body: PostAgentUnenrollResponse = { - success: true, - }; + const body: PostAgentUnenrollResponse = {}; return response.ok({ body }); } catch (e) { return response.customError({ diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts index cc178e1038e2e..7eb3df2346106 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts @@ -49,7 +49,6 @@ export const getAgentPoliciesHandler: RequestHandler< total, page, perPage, - success: true, }; await bluebird.map( @@ -82,7 +81,6 @@ export const getOneAgentPolicyHandler: RequestHandler { const soClient = context.core.savedObjects.client; @@ -16,7 +16,6 @@ export const getSettingsHandler: RequestHandler = async (context, request, respo try { const settings = await settingsService.getSettings(soClient); const body = { - success: true, item: settings, }; return response.ok({ body }); @@ -40,10 +39,13 @@ export const putSettingsHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const user = await appContextService.getSecurity()?.authc.getCurrentUser(request); try { const settings = await settingsService.saveSettings(soClient, request.body); + await agentPolicyService.bumpAllAgentPolicies(soClient, { + user: user || undefined, + }); const body = { - success: true, item: settings, }; return response.ok({ body }); diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts index 1daa63800f4ee..ee7dab6ef1a8b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts @@ -9,7 +9,7 @@ import { outputService, appContextService } from '../../services'; import { GetFleetStatusResponse, PostIngestSetupResponse } from '../../../common'; import { setupIngestManager, setupFleet } from '../../services/setup'; import { PostFleetSetupRequestSchema } from '../../types'; -import { IngestManagerError, getHTTPResponseCode } from '../../errors'; +import { defaultIngestErrorHandler } from '../../errors'; export const getFleetStatusHandler: RequestHandler = async (context, request, response) => { const soClient = context.core.savedObjects.client; @@ -46,11 +46,8 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re return response.ok({ body, }); - } catch (e) { - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; @@ -70,44 +67,22 @@ export const createFleetSetupHandler: RequestHandler< return response.ok({ body: { isInitialized: true }, }); - } catch (e) { - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; export const ingestManagerSetupHandler: RequestHandler = async (context, request, response) => { const soClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; - const logger = appContextService.getLogger(); + try { const body: PostIngestSetupResponse = { isInitialized: true }; await setupIngestManager(soClient, callCluster); return response.ok({ body, }); - } catch (e) { - if (e instanceof IngestManagerError) { - logger.error(e.message); - return response.customError({ - statusCode: getHTTPResponseCode(e), - body: { message: e.message }, - }); - } - if (e.isBoom) { - logger.error(e.output.payload.message); - return response.customError({ - statusCode: e.output.statusCode, - body: { message: e.output.payload.message }, - }); - } - logger.error(e.message); - logger.error(e.stack); - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index 1bbe3b71bf919..aff8e607622d4 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -23,6 +23,7 @@ import { migrateAgentPolicyToV7100, migrateEnrollmentApiKeysToV7100, migratePackagePolicyToV7100, + migrateSettingsToV7100, } from './migrations/to_v7_10_0'; /* @@ -43,11 +44,14 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { properties: { agent_auto_upgrade: { type: 'keyword' }, package_auto_upgrade: { type: 'keyword' }, - kibana_url: { type: 'keyword' }, + kibana_urls: { type: 'keyword' }, kibana_ca_sha256: { type: 'keyword' }, has_seen_add_data_notice: { type: 'boolean', index: false }, }, }, + migrations: { + '7.10.0': migrateSettingsToV7100, + }, }, [AGENT_SAVED_OBJECT_TYPE]: { name: AGENT_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/migrations/to_v7_10_0.ts b/x-pack/plugins/ingest_manager/server/saved_objects/migrations/to_v7_10_0.ts index b60903dbd2bd0..5e36ce46c099b 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/migrations/to_v7_10_0.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/migrations/to_v7_10_0.ts @@ -5,7 +5,14 @@ */ import { SavedObjectMigrationFn } from 'kibana/server'; -import { Agent, AgentEvent, AgentPolicy, PackagePolicy, EnrollmentAPIKey } from '../../types'; +import { + Agent, + AgentEvent, + AgentPolicy, + PackagePolicy, + EnrollmentAPIKey, + Settings, +} from '../../types'; export const migrateAgentToV7100: SavedObjectMigrationFn< Exclude & { @@ -72,3 +79,16 @@ export const migratePackagePolicyToV7100: SavedObjectMigrationFn< return packagePolicyDoc; }; + +export const migrateSettingsToV7100: SavedObjectMigrationFn< + Exclude & { + kibana_url: string; + }, + Settings +> = (settingsDoc) => { + settingsDoc.attributes.kibana_urls = [settingsDoc.attributes.kibana_url]; + // @ts-expect-error + delete settingsDoc.attributes.kibana_url; + + return settingsDoc; +}; diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts index dc2a89c661ac3..d9dffa03b2290 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts @@ -19,6 +19,24 @@ function getSavedObjectMock(agentPolicyAttributes: any) { attributes: agentPolicyAttributes, }; }); + mock.find.mockImplementation(async (options) => { + return { + saved_objects: [ + { + id: '93f74c0-e876-11ea-b7d3-8b2acec6f75c', + attributes: { + kibana_urls: ['http://localhost:5603'], + }, + type: 'ingest_manager_settings', + score: 1, + references: [], + }, + ], + total: 1, + page: 1, + per_page: 1, + }; + }); return mock; } @@ -43,7 +61,7 @@ jest.mock('./output', () => { describe('agent policy', () => { describe('getFullAgentPolicy', () => { - it('should return a policy without monitoring if not monitoring is not enabled', async () => { + it('should return a policy without monitoring if monitoring is not enabled', async () => { const soClient = getSavedObjectMock({ revision: 1, }); @@ -61,6 +79,11 @@ describe('agent policy', () => { }, inputs: [], revision: 1, + fleet: { + kibana: { + hosts: ['http://localhost:5603'], + }, + }, agent: { monitoring: { enabled: false, @@ -90,6 +113,11 @@ describe('agent policy', () => { }, inputs: [], revision: 1, + fleet: { + kibana: { + hosts: ['http://localhost:5603'], + }, + }, agent: { monitoring: { use_output: 'default', @@ -120,6 +148,11 @@ describe('agent policy', () => { }, inputs: [], revision: 1, + fleet: { + kibana: { + hosts: ['http://localhost:5603'], + }, + }, agent: { monitoring: { use_output: 'default', diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts index 21bc7b021e83a..a03a3b7f59fba 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { uniq } from 'lodash'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract, SavedObjectsBulkUpdateResponse } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; import { DEFAULT_AGENT_POLICY, @@ -25,6 +25,7 @@ import { listAgents } from './agents'; import { packagePolicyService } from './package_policy'; import { outputService } from './output'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; +import { getSettings } from './settings'; const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; @@ -260,6 +261,25 @@ class AgentPolicyService { ): Promise { return this._update(soClient, id, {}, options?.user); } + public async bumpAllAgentPolicies( + soClient: SavedObjectsClientContract, + options?: { user?: AuthenticatedUser } + ): Promise>> { + const currentPolicies = await soClient.find({ + type: SAVED_OBJECT_TYPE, + fields: ['revision'], + }); + const bumpedPolicies = currentPolicies.saved_objects.map((policy) => { + policy.attributes = { + ...policy.attributes, + revision: policy.attributes.revision + 1, + updated_at: new Date().toISOString(), + updated_by: options?.user ? options.user.username : 'system', + }; + return policy; + }); + return soClient.bulkUpdate(bumpedPolicies); + } public async assignPackagePolicies( soClient: SavedObjectsClientContract, @@ -360,7 +380,6 @@ class AgentPolicyService { await this.triggerAgentPolicyUpdatedEvent(soClient, 'deleted', id); return { id, - success: true, }; } @@ -370,6 +389,7 @@ class AgentPolicyService { options?: { standalone: boolean } ): Promise { let agentPolicy; + const standalone = options?.standalone; try { agentPolicy = await this.get(soClient, id); @@ -435,6 +455,22 @@ class AgentPolicyService { }), }; + // only add settings if not in standalone + if (!standalone) { + let settings; + try { + settings = await getSettings(soClient); + } catch (error) { + throw new Error('Default settings is not setup'); + } + if (!settings.kibana_urls) throw new Error('kibana_urls is missing'); + fullAgentPolicy.fleet = { + kibana: { + hosts: settings.kibana_urls, + }, + }; + } + return fullAgentPolicy; } } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts index e10d013fe84a9..eddfb0e64b84b 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts @@ -170,7 +170,10 @@ export function agentCheckinStateNewActionsFactory() { } const stream$ = agentPolicy$.pipe( - timeout(appContextService.getConfig()?.fleet.pollingRequestTimeout || 0), + timeout( + // Set a timeout 3s before the real timeout to have a chance to respond an empty response before socket timeout + Math.max((appContextService.getConfig()?.fleet.pollingRequestTimeout ?? 0) - 3000, 3000) + ), filter((agentPolicy) => shouldCreateAgentPolicyAction(agent, agentPolicy)), rateLimiter(), mergeMap((agentPolicy) => createAgentActionFromAgentPolicy(soClient, agent, agentPolicy)), diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.test.ts index f836a133a78a0..2f1d400018740 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { fetchUrl } from './requests'; -import { RegistryError } from '../../../errors'; +import { RegistryError, RegistryConnectionError, RegistryResponseError } from '../../../errors'; jest.mock('node-fetch'); const { Response, FetchError } = jest.requireActual('node-fetch'); @@ -53,13 +53,7 @@ describe('setupIngestManager', () => { throw new FetchError('message 3', 'system', { code: 'ESOMETHING' }); }) // this one succeeds - .mockImplementationOnce(() => Promise.resolve(new Response(successValue))) - .mockImplementationOnce(() => { - throw new FetchError('message 5', 'system', { code: 'ESOMETHING' }); - }) - .mockImplementationOnce(() => { - throw new FetchError('message 6', 'system', { code: 'ESOMETHING' }); - }); + .mockImplementationOnce(() => Promise.resolve(new Response(successValue))); const promise = fetchUrl(''); await expect(promise).resolves.toEqual(successValue); @@ -69,7 +63,7 @@ describe('setupIngestManager', () => { expect(actualResultsOrder).toEqual(['throw', 'throw', 'throw', 'return']); }); - it('or error after 1 failure & 5 retries with RegistryError', async () => { + it('or error after 1 failure & 5 retries with RegistryConnectionError', async () => { fetchMock .mockImplementationOnce(() => { throw new FetchError('message 1', 'system', { code: 'ESOMETHING' }); @@ -88,21 +82,93 @@ describe('setupIngestManager', () => { }) .mockImplementationOnce(() => { throw new FetchError('message 6', 'system', { code: 'ESOMETHING' }); - }) - .mockImplementationOnce(() => { - throw new FetchError('message 7', 'system', { code: 'ESOMETHING' }); - }) - .mockImplementationOnce(() => { - throw new FetchError('message 8', 'system', { code: 'ESOMETHING' }); }); const promise = fetchUrl(''); - await expect(promise).rejects.toThrow(RegistryError); + await expect(promise).rejects.toThrow(RegistryConnectionError); // doesn't retry after 1 failure & 5 failed retries expect(fetchMock).toHaveBeenCalledTimes(6); const actualResultsOrder = fetchMock.mock.results.map(({ type }: { type: string }) => type); expect(actualResultsOrder).toEqual(['throw', 'throw', 'throw', 'throw', 'throw', 'throw']); }); }); + + describe('4xx or 5xx from Registry become RegistryResponseError', () => { + it('404', async () => { + fetchMock.mockImplementationOnce(() => ({ + ok: false, + status: 404, + statusText: 'Not Found', + url: 'https://example.com', + })); + const promise = fetchUrl(''); + await expect(promise).rejects.toThrow(RegistryResponseError); + await expect(promise).rejects.toThrow( + `'404 Not Found' error response from package registry at https://example.com` + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('429', async () => { + fetchMock.mockImplementationOnce(() => ({ + ok: false, + status: 429, + statusText: 'Too Many Requests', + url: 'https://example.com', + })); + const promise = fetchUrl(''); + await expect(promise).rejects.toThrow(RegistryResponseError); + await expect(promise).rejects.toThrow( + `'429 Too Many Requests' error response from package registry at https://example.com` + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('500', async () => { + fetchMock.mockImplementationOnce(() => ({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + url: 'https://example.com', + })); + const promise = fetchUrl(''); + await expect(promise).rejects.toThrow(RegistryResponseError); + await expect(promise).rejects.toThrow( + `'500 Internal Server Error' error response from package registry at https://example.com` + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('url in RegistryResponseError message is response.url || requested_url', () => { + it('given response.url, use that', async () => { + fetchMock.mockImplementationOnce(() => ({ + ok: false, + status: 404, + statusText: 'Not Found', + url: 'https://example.com/?from_response=true', + })); + const promise = fetchUrl('https://example.com/?requested=true'); + await expect(promise).rejects.toThrow(RegistryResponseError); + await expect(promise).rejects.toThrow( + `'404 Not Found' error response from package registry at https://example.com/?from_response=true` + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('no response.url, use requested url', async () => { + fetchMock.mockImplementationOnce(() => ({ + ok: false, + status: 404, + statusText: 'Not Found', + })); + const promise = fetchUrl('https://example.com/?requested=true'); + await expect(promise).rejects.toThrow(RegistryResponseError); + await expect(promise).rejects.toThrow( + `'404 Not Found' error response from package registry at https://example.com/?requested=true` + ); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + }); }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.ts index 5939dc204aae6..e549d6b1f71aa 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/requests.ts @@ -7,7 +7,7 @@ import fetch, { FetchError, Response } from 'node-fetch'; import pRetry from 'p-retry'; import { streamToString } from './streams'; -import { RegistryError } from '../../../errors'; +import { RegistryError, RegistryConnectionError, RegistryResponseError } from '../../../errors'; type FailedAttemptErrors = pRetry.FailedAttemptError | FetchError | Error; @@ -19,10 +19,13 @@ async function registryFetch(url: string) { return response; } else { // 4xx & 5xx responses - // exit without retry & throw RegistryError - throw new pRetry.AbortError( - new RegistryError(`Error connecting to package registry at ${url}: ${response.statusText}`) - ); + const { status, statusText, url: resUrl } = response; + const message = `'${status} ${statusText}' error response from package registry at ${ + resUrl || url + }`; + const responseError = new RegistryResponseError(message); + + throw new pRetry.AbortError(responseError); } } @@ -38,17 +41,24 @@ export async function getResponse(url: string): Promise { // and let the others through without retrying // // throwing in onFailedAttempt will abandon all retries & fail the request - // we only want to retry system errors, so throw a RegistryError for everything else + // we only want to retry system errors, so re-throw for everything else if (!isSystemError(error)) { - throw new RegistryError( - `Error connecting to package registry at ${url}: ${error.message}` - ); + throw error; } }, }); return response; - } catch (e) { - throw new RegistryError(`Error connecting to package registry at ${url}: ${e.message}`); + } catch (error) { + // isSystemError here means we didn't succeed after max retries + if (isSystemError(error)) { + throw new RegistryConnectionError(`Error connecting to package registry: ${error.message}`); + } + // don't wrap our own errors + if (error instanceof RegistryError) { + throw error; + } else { + throw new RegistryError(error); + } } } diff --git a/x-pack/plugins/ingest_manager/server/services/settings.ts b/x-pack/plugins/ingest_manager/server/services/settings.ts index f1c09746d9abd..25223fbc08535 100644 --- a/x-pack/plugins/ingest_manager/server/services/settings.ts +++ b/x-pack/plugins/ingest_manager/server/services/settings.ts @@ -5,7 +5,15 @@ */ import Boom from 'boom'; import { SavedObjectsClientContract } from 'kibana/server'; -import { GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, SettingsSOAttributes, Settings } from '../../common'; +import url from 'url'; +import { + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + SettingsSOAttributes, + Settings, + decodeCloudId, + BaseSettings, +} from '../../common'; +import { appContextService } from './app_context'; export async function getSettings(soClient: SavedObjectsClientContract): Promise { const res = await soClient.find({ @@ -25,7 +33,7 @@ export async function getSettings(soClient: SavedObjectsClientContract): Promise export async function saveSettings( soClient: SavedObjectsClientContract, newData: Partial> -): Promise { +): Promise & Pick> { try { const settings = await getSettings(soClient); @@ -41,10 +49,11 @@ export async function saveSettings( }; } catch (e) { if (e.isBoom && e.output.statusCode === 404) { - const res = await soClient.create( - GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, - newData - ); + const defaultSettings = createDefaultSettings(); + const res = await soClient.create(GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, { + ...defaultSettings, + ...newData, + }); return { id: res.id, @@ -55,3 +64,26 @@ export async function saveSettings( throw e; } } + +export function createDefaultSettings(): BaseSettings { + const http = appContextService.getHttpSetup(); + const serverInfo = http.getServerInfo(); + const basePath = http.basePath; + + const cloud = appContextService.getCloud(); + const cloudId = cloud?.isCloudEnabled && cloud.cloudId; + const cloudUrl = cloudId && decodeCloudId(cloudId)?.kibanaUrl; + const flagsUrl = appContextService.getConfig()?.fleet?.kibana?.host; + const defaultUrl = url.format({ + protocol: serverInfo.protocol, + hostname: serverInfo.hostname, + port: serverInfo.port, + pathname: basePath.serverBasePath, + }); + + return { + agent_auto_upgrade: true, + package_auto_upgrade: true, + kibana_urls: [cloudUrl || flagsUrl || defaultUrl].flat(), + }; +} diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index fd5d94a71d672..ec3a05a4fa390 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import url from 'url'; import uuid from 'uuid'; import { SavedObjectsClientContract } from 'src/core/server'; import { CallESAsCurrentUser } from '../types'; @@ -22,14 +21,13 @@ import { Installation, Output, DEFAULT_AGENT_POLICIES_PACKAGES, - decodeCloudId, } from '../../common'; import { getPackageInfo } from './epm/packages'; import { packagePolicyService } from './package_policy'; import { generateEnrollmentAPIKey } from './api_keys'; import { settingsService } from '.'; -import { appContextService } from './app_context'; import { awaitIfPending } from './setup_utils'; +import { createDefaultSettings } from './settings'; const FLEET_ENROLL_USERNAME = 'fleet_enroll'; const FLEET_ENROLL_ROLE = 'fleet_enroll'; @@ -58,26 +56,8 @@ async function createSetupSideEffects( ensureDefaultIndices(callCluster), settingsService.getSettings(soClient).catch((e: any) => { if (e.isBoom && e.output.statusCode === 404) { - const http = appContextService.getHttpSetup(); - const serverInfo = http.getServerInfo(); - const basePath = http.basePath; - - const cloud = appContextService.getCloud(); - const cloudId = cloud?.isCloudEnabled && cloud.cloudId; - const cloudUrl = cloudId && decodeCloudId(cloudId)?.kibanaUrl; - const flagsUrl = appContextService.getConfig()?.fleet?.kibana?.host; - const defaultUrl = url.format({ - protocol: serverInfo.protocol, - hostname: serverInfo.hostname, - port: serverInfo.port, - pathname: basePath.serverBasePath, - }); - - return settingsService.saveSettings(soClient, { - agent_auto_upgrade: true, - package_auto_upgrade: true, - kibana_url: cloudUrl || flagsUrl || defaultUrl, - }); + const defaultSettings = createDefaultSettings(); + return settingsService.saveSettings(soClient, defaultSettings); } return Promise.reject(e); diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index aabe4bd3e3597..e01568cfbb3c9 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -63,6 +63,9 @@ export { IndexTemplateMappings, Settings, SettingsSOAttributes, + // Agent Request types + PostAgentEnrollRequest, + PostAgentCheckinRequest, } from '../../common'; export type CallESAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index 3302b0ab84ba8..43ee0c89126e9 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -5,12 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { - AckEventSchema, - NewAgentEventSchema, - AgentTypeSchema, - NewAgentActionSchema, -} from '../models'; +import { NewAgentActionSchema } from '../models'; export const GetAgentsRequestSchema = { query: schema.object({ @@ -27,37 +22,134 @@ export const GetOneAgentRequestSchema = { }), }; -export const PostAgentCheckinRequestSchema = { - params: schema.object({ - agentId: schema.string(), - }), - body: schema.object({ - status: schema.maybe( - schema.oneOf([schema.literal('online'), schema.literal('error'), schema.literal('degraded')]) - ), - local_metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())), - events: schema.maybe(schema.arrayOf(NewAgentEventSchema)), - }), +export const PostAgentCheckinRequestParamsJSONSchema = { + type: 'object', + properties: { + agentId: { type: 'string' }, + }, + required: ['agentId'], }; -export const PostAgentEnrollRequestSchema = { - body: schema.object({ - type: AgentTypeSchema, - shared_id: schema.maybe(schema.string()), - metadata: schema.object({ - local: schema.recordOf(schema.string(), schema.any()), - user_provided: schema.recordOf(schema.string(), schema.any()), - }), - }), +export const PostAgentCheckinRequestBodyJSONSchema = { + type: 'object', + properties: { + status: { type: 'string', enum: ['online', 'error', 'degraded'] }, + local_metadata: { + additionalProperties: { + anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'object' }], + }, + }, + events: { + type: 'array', + items: { + type: 'object', + properties: { + type: { type: 'string', enum: ['STATE', 'ERROR', 'ACTION_RESULT', 'ACTION'] }, + subtype: { + type: 'string', + enum: [ + 'RUNNING', + 'STARTING', + 'IN_PROGRESS', + 'CONFIG', + 'FAILED', + 'STOPPING', + 'STOPPED', + 'DEGRADED', + 'DATA_DUMP', + 'ACKNOWLEDGED', + 'UNKNOWN', + ], + }, + timestamp: { type: 'string' }, + message: { type: 'string' }, + payload: { type: 'object', additionalProperties: true }, + agent_id: { type: 'string' }, + action_id: { type: 'string' }, + policy_id: { type: 'string' }, + stream_id: { type: 'string' }, + }, + required: ['type', 'subtype', 'timestamp', 'message', 'agent_id'], + additionalProperties: false, + }, + }, + }, + additionalProperties: false, }; -export const PostAgentAcksRequestSchema = { - body: schema.object({ - events: schema.arrayOf(AckEventSchema), - }), - params: schema.object({ - agentId: schema.string(), - }), +export const PostAgentEnrollRequestBodyJSONSchema = { + type: 'object', + properties: { + type: { type: 'string', enum: ['EPHEMERAL', 'PERMANENT', 'TEMPORARY'] }, + shared_id: { type: 'string' }, + metadata: { + type: 'object', + properties: { + local: { + type: 'object', + additionalProperties: true, + }, + user_provided: { + type: 'object', + additionalProperties: true, + }, + }, + additionalProperties: false, + required: ['local', 'user_provided'], + }, + }, + additionalProperties: false, + required: ['type', 'metadata'], +}; + +export const PostAgentAcksRequestParamsJSONSchema = { + type: 'object', + properties: { + agentId: { type: 'string' }, + }, + required: ['agentId'], +}; + +export const PostAgentAcksRequestBodyJSONSchema = { + type: 'object', + properties: { + events: { + type: 'array', + item: { + type: 'object', + properties: { + type: { type: 'string', enum: ['STATE', 'ERROR', 'ACTION_RESULT', 'ACTION'] }, + subtype: { + type: 'string', + enum: [ + 'RUNNING', + 'STARTING', + 'IN_PROGRESS', + 'CONFIG', + 'FAILED', + 'STOPPING', + 'STOPPED', + 'DEGRADED', + 'DATA_DUMP', + 'ACKNOWLEDGED', + 'UNKNOWN', + ], + }, + timestamp: { type: 'string' }, + message: { type: 'string' }, + payload: { type: 'object', additionalProperties: true }, + agent_id: { type: 'string' }, + action_id: { type: 'string' }, + policy_id: { type: 'string' }, + stream_id: { type: 'string' }, + }, + required: ['type', 'subtype', 'timestamp', 'message', 'agent_id', 'action_id'], + additionalProperties: false, + }, + }, + }, + additionalProperties: false, + required: ['events'], }; export const PostNewAgentActionRequestSchema = { diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts index baee9f79d9317..35718491c9224 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.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 { isDiffPathProtocol } from '../../../common'; export const GetSettingsRequestSchema = {}; @@ -11,7 +12,15 @@ export const PutSettingsRequestSchema = { body: schema.object({ agent_auto_upgrade: schema.maybe(schema.boolean()), package_auto_upgrade: schema.maybe(schema.boolean()), - kibana_url: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), + kibana_urls: schema.maybe( + schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }), { + validate: (value) => { + if (isDiffPathProtocol(value)) { + return 'Protocol and path must be the same for each URL'; + } + }, + }) + ), kibana_ca_sha256: schema.maybe(schema.string()), has_seen_add_data_notice: schema.maybe(schema.boolean()), }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx index f183386d5927d..9e777de0e2edf 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FunctionComponent, useRef, useState } from 'react'; +import React, { FunctionComponent, useRef, useState, useCallback } from 'react'; import { EuiConfirmModal, EuiOverlayMask, EuiSpacer, EuiText, EuiCallOut } from '@elastic/eui'; import { JsonEditor, OnJsonEditorUpdateHandler } from '../../../../../shared_imports'; @@ -66,10 +66,12 @@ export const ModalProvider: FunctionComponent = ({ onDone, children }) => raw: defaultValueRaw, }, }); - const onJsonUpdate: OnJsonEditorUpdateHandler = (jsonUpdateData) => { + + const onJsonUpdate: OnJsonEditorUpdateHandler = useCallback((jsonUpdateData) => { setIsValidJson(jsonUpdateData.validate()); jsonContent.current = jsonUpdateData; - }; + }, []); + return ( <> {children(() => setIsModalVisible(true))} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx index 7fb92e89c9f68..e00f9c002e5bc 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx @@ -14,6 +14,11 @@ interface Props { editorProps: { [key: string]: any }; } +const defaultEditorOptions = { + minimap: { enabled: false }, + lineNumbers: 'off', +}; + export const XJsonEditor: FunctionComponent = ({ field, editorProps }) => { const { value, setValue } = field; const { xJson, setXJson, convertToJson } = Monaco.useXJsonMode(value); @@ -31,7 +36,7 @@ export const XJsonEditor: FunctionComponent = ({ field, editorProps }) => editorProps={{ value: xJson, languageId: XJsonLang.ID, - options: { minimap: { enabled: false } }, + options: defaultEditorOptions, onChange, ...editorProps, }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx index 9adb3957ea9f4..bda64c0a75612 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx @@ -33,10 +33,22 @@ export const ProcessorSettingsFields: FunctionComponent = ({ processor }) const formDescriptor = getProcessorDescriptor(type as any); if (formDescriptor?.FieldsComponent) { + const renderedFields = ( + + ); return ( <> - - + {renderedFields ? ( + <> + {renderedFields} + + + ) : ( + renderedFields + )} ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx index 8089b8e7dfad3..e1048d627f66c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx @@ -16,12 +16,12 @@ import { } from '../../../../../../../shared_imports'; import { TextEditor } from '../../field_components'; -import { to, from } from '../shared'; +import { to, from, EDITOR_PX_HEIGHT } from '../shared'; const ignoreFailureConfig: FieldConfig = { defaultValue: false, deserializer: to.booleanOrUndef, - serializer: from.defaultBoolToUndef(false), + serializer: from.undefinedIfValue(false), label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureFieldLabel', { @@ -64,8 +64,11 @@ export const CommonProcessorFields: FunctionComponent = () => { componentProps={{ editorProps: { languageId: 'painless', - height: 75, - options: { minimap: { enabled: false } }, + height: EDITOR_PX_HEIGHT.extraSmall, + options: { + lineNumbers: 'off', + minimap: { enabled: false }, + }, }, }} path="fields.if" diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx index 63ebb47dfc573..3d38f9238cdd1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx @@ -22,7 +22,7 @@ export const fieldsConfig: FieldsConfig = { type: FIELD_TYPES.TOGGLE, defaultValue: false, deserializer: to.booleanOrUndef, - serializer: from.defaultBoolToUndef(false), + serializer: from.undefinedIfValue(false), label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreMissingFieldLabel', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx index 9bf44425a7c5d..f808c8c11ff0a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx @@ -21,8 +21,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.commonFields.targetFieldHelpText', { - defaultMessage: - 'The field to assign the joined value to. If empty, the field is updated in-place.', + defaultMessage: 'Field to assign the value to. If empty, the field is updated in-place.', } ), }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx index 82fdc81e0a843..c2aab62cf8933 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx @@ -17,6 +17,7 @@ import { const { emptyField, isJsonField } = fieldValidators; import { XJsonEditor } from '../field_components'; +import { EDITOR_PX_HEIGHT } from './shared'; const customConfig: FieldConfig = { type: FIELD_TYPES.TEXT, @@ -78,7 +79,7 @@ export const Custom: FunctionComponent = ({ defaultOptions }) => { componentProps={{ editorProps: { 'data-test-subj': 'processorOptionsEditor', - height: 300, + height: EDITOR_PX_HEIGHT.large, 'aria-label': i18n.translate( 'xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldAriaLabel', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx index 5f9f55ced1a25..344855304e4fd 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx @@ -20,6 +20,7 @@ import { import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { EDITOR_PX_HEIGHT } from './shared'; const { emptyField } = fieldValidators; @@ -80,7 +81,7 @@ export const Dissect: FunctionComponent = () => { component={TextEditor} componentProps={{ editorProps: { - height: 75, + height: EDITOR_PX_HEIGHT.extraSmall, options: { minimap: { enabled: false } }, }, }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx index 5986374b338cf..ba1c55b731cc9 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx @@ -81,7 +81,7 @@ const fieldsConfig: FieldsConfig = { type: FIELD_TYPES.TOGGLE, defaultValue: true, deserializer: to.booleanOrUndef, - serializer: from.defaultBoolToUndef, + serializer: from.undefinedIfValue, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.enrichForm.overrideFieldLabel', { defaultMessage: 'Override', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx index ce606af086893..c32a85310d21f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx @@ -12,7 +12,7 @@ import { FIELD_TYPES, fieldValidators, UseField } from '../../../../../../shared import { XJsonEditor } from '../field_components'; import { FieldNameField } from './common_fields/field_name_field'; -import { FieldsConfig, to } from './shared'; +import { FieldsConfig, to, EDITOR_PX_HEIGHT } from './shared'; const { emptyField, isJsonField } = fieldValidators; @@ -31,15 +31,18 @@ const fieldsConfig: FieldsConfig = { validations: [ { validator: emptyField( - i18n.translate('xpack.ingestPipelines.pipelineEditor.failForm.processorRequiredError', { - defaultMessage: 'A processor is required.', - }) + i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.foreachForm.processorRequiredError', + { + defaultMessage: 'A processor is required.', + } + ) ), }, { validator: isJsonField( i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.failForm.processorInvalidJsonError', + 'xpack.ingestPipelines.pipelineEditor.foreachForm.processorInvalidJsonError', { defaultMessage: 'Invalid JSON', } @@ -64,9 +67,9 @@ export const Foreach: FunctionComponent = () => { component={XJsonEditor} componentProps={{ editorProps: { - height: 200, + height: EDITOR_PX_HEIGHT.medium, 'aria-label': i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldAriaLabel', + 'xpack.ingestPipelines.pipelineEditor.foreachForm.optionsFieldAriaLabel', { defaultMessage: 'Configuration JSON editor', } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx index 9bb1d679938ed..c0624c988061c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx @@ -61,7 +61,7 @@ const fieldsConfig: FieldsConfig = { type: FIELD_TYPES.TOGGLE, defaultValue: true, deserializer: to.booleanOrUndef, - serializer: from.defaultBoolToUndef(true), + serializer: from.undefinedIfValue(true), label: i18n.translate('xpack.ingestPipelines.pipelineEditor.geoIPForm.firstOnlyFieldLabel', { defaultMessage: 'First only', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx index d021038fda94f..c5c6adbe2a7a8 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx @@ -19,7 +19,7 @@ import { XJsonEditor } from '../field_components'; import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; -import { FieldsConfig, to, from } from './shared'; +import { FieldsConfig, to, from, EDITOR_PX_HEIGHT } from './shared'; const { emptyField, isJsonField } = fieldValidators; @@ -80,7 +80,7 @@ const fieldsConfig: FieldsConfig = { type: FIELD_TYPES.TOGGLE, defaultValue: false, deserializer: to.booleanOrUndef, - serializer: from.defaultBoolToUndef(false), + serializer: from.undefinedIfValue(false), label: i18n.translate('xpack.ingestPipelines.pipelineEditor.grokForm.traceMatchFieldLabel', { defaultMessage: 'Trace match', }), @@ -110,7 +110,7 @@ export const Grok: FunctionComponent = () => { config={fieldsConfig.pattern_definitions} componentProps={{ editorProps: { - height: 200, + height: EDITOR_PX_HEIGHT.medium, 'aria-label': i18n.translate( 'xpack.ingestPipelines.pipelineEditor.grokForm.patternDefinitionsAriaLabel', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx index a0bda245d667b..a42df6873d57b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx @@ -13,7 +13,7 @@ import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../.. import { TextEditor } from '../field_components'; -import { FieldsConfig } from './shared'; +import { EDITOR_PX_HEIGHT, FieldsConfig } from './shared'; import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { TargetField } from './common_fields/target_field'; @@ -78,7 +78,7 @@ export const Gsub: FunctionComponent = () => { component={TextEditor} componentProps={{ editorProps: { - height: 75, + height: EDITOR_PX_HEIGHT.extraSmall, options: { minimap: { enabled: false } }, }, }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts index 4974361bf0410..e83560b4a44ce 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts @@ -24,3 +24,15 @@ export { HtmlStrip } from './html_strip'; export { Inference } from './inference'; export { Join } from './join'; export { Json } from './json'; +export { Kv } from './kv'; +export { Lowercase } from './lowercase'; +export { Pipeline } from './pipeline'; +export { Remove } from './remove'; +export { Rename } from './rename'; +export { Script } from './script'; +export { SetProcessor } from './set'; +export { SetSecurityUser } from './set_security_user'; +export { Split } from './split'; +export { Sort } from './sort'; + +export { FormFieldsComponent } from './shared'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx index 68281fc11f340..85f995fa77cea 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx @@ -21,7 +21,7 @@ import { XJsonEditor } from '../field_components'; import { TargetField } from './common_fields/target_field'; -import { FieldsConfig, to, from } from './shared'; +import { FieldsConfig, to, from, EDITOR_PX_HEIGHT } from './shared'; const { emptyField, isJsonField } = fieldValidators; @@ -177,7 +177,7 @@ export const Inference: FunctionComponent = () => { component={XJsonEditor} componentProps={{ editorProps: { - height: 200, + height: EDITOR_PX_HEIGHT.medium, options: { minimap: { enabled: false } }, }, }} @@ -192,7 +192,7 @@ export const Inference: FunctionComponent = () => { component={XJsonEditor} componentProps={{ editorProps: { - height: 200, + height: EDITOR_PX_HEIGHT.medium, options: { minimap: { enabled: false } }, }, }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx index c35a5b463f573..ab077d3337f63 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx @@ -35,7 +35,7 @@ const fieldsConfig: FieldsConfig = { { validator: emptyField( i18n.translate('xpack.ingestPipelines.pipelineEditor.joinForm.separatorRequiredError', { - defaultMessage: 'A separator value is required.', + defaultMessage: 'A value is required.', }) ), }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx index 5c4c53b65b6dc..b68b398325085 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx @@ -29,7 +29,7 @@ const fieldsConfig: FieldsConfig = { defaultMessage: 'Add to root', }), deserializer: to.booleanOrUndef, - serializer: from.defaultBoolToUndef(false), + serializer: from.undefinedIfValue(false), helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.jsonForm.addToRootFieldHelpText', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx new file mode 100644 index 0000000000000..f51bf19ad180a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx @@ -0,0 +1,202 @@ +/* + * 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 React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + ComboBoxField, + ToggleField, +} from '../../../../../../shared_imports'; + +import { FieldsConfig, from, to } from './shared'; +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + field_split: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitFieldLabel', { + defaultMessage: 'Field split', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitHelpText', { + defaultMessage: 'Regex pattern for splitting key-value pairs.', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitRequiredError', { + defaultMessage: 'A value is required.', + }) + ), + }, + ], + }, + value_split: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitFieldLabel', { + defaultMessage: 'Value split', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitHelpText', { + defaultMessage: 'Regex pattern for splitting the key from the value within a key-value pair.', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitRequiredError', { + defaultMessage: 'A value is required.', + }) + ), + }, + ], + }, + + /* Optional fields config */ + include_keys: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: to.arrayOfStrings, + serializer: from.optionalArrayOfStrings, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysFieldLabel', { + defaultMessage: 'Include keys', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysHelpText', { + defaultMessage: + 'List of keys to filter and insert into document. Defaults to including all keys.', + }), + }, + + exclude_keys: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: to.arrayOfStrings, + serializer: from.optionalArrayOfStrings, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysFieldLabel', { + defaultMessage: 'Exclude keys', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysHelpText', { + defaultMessage: 'List of keys to exclude from document.', + }), + }, + + prefix: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.prefixFieldLabel', { + defaultMessage: 'Prefix', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.prefixHelpText', { + defaultMessage: 'Prefix to be added to extracted keys.', + }), + }, + + trim_key: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimKeyFieldLabel', { + defaultMessage: 'Trim key', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimKeyHelpText', { + defaultMessage: 'Characters to trim from extracted keys.', + }), + }, + + trim_value: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimValueFieldLabel', { + defaultMessage: 'Trim value', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimValueHelpText', { + defaultMessage: 'Characters to trim from extracted values.', + }), + }, + + strip_brackets: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + deserializer: to.booleanOrUndef, + serializer: from.undefinedIfValue(false), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.stripBracketsFieldLabel', { + defaultMessage: 'Strip brackets', + }), + helpText: ( + {'()'}, + angle: <>, + square: {'[]'}, + singleQuote: {"'"}, + doubleQuote: {'"'}, + }} + /> + ), + }, +}; + +export const Kv: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx new file mode 100644 index 0000000000000..9db313a05007f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; + +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +export const Lowercase: FunctionComponent = () => { + return ( + <> + + + {'field'}, + }} + /> + } + /> + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx new file mode 100644 index 0000000000000..c785cf935833d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx @@ -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 React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + name: { + type: FIELD_TYPES.TEXT, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldLabel', + { + defaultMessage: 'Pipeline name', + } + ), + deserializer: String, + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldHelpText', + { + defaultMessage: 'Name of the pipeline to execute.', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameRequiredError', + { + defaultMessage: 'A value is required.', + } + ) + ), + }, + ], + }, +}; + +export const Pipeline: FunctionComponent = () => { + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx new file mode 100644 index 0000000000000..3e90ce2b76f7b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx @@ -0,0 +1,54 @@ +/* + * 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 React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FIELD_TYPES, + UseField, + ComboBoxField, + fieldValidators, +} from '../../../../../../shared_imports'; + +import { FieldsConfig, to } from './shared'; + +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + field: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: to.arrayOfStrings, + serializer: (v: string[]) => (v.length === 1 ? v[0] : v), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameField', { + defaultMessage: 'Fields', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameHelpText', { + defaultMessage: 'Fields to be removed.', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameRequiredError', { + defaultMessage: 'A value is required.', + }) + ), + }, + ], + }, +}; + +export const Remove: FunctionComponent = () => { + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx new file mode 100644 index 0000000000000..8b796d9664586 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx @@ -0,0 +1,51 @@ +/* + * 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 React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { fieldValidators } from '../../../../../../shared_imports'; + +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +const { emptyField } = fieldValidators; + +export const Rename: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx new file mode 100644 index 0000000000000..ae0bbbb490ae9 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx @@ -0,0 +1,186 @@ +/* + * 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 React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode, EuiSwitch, EuiFormRow } from '@elastic/eui'; + +import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../../shared_imports'; + +import { XJsonEditor, TextEditor } from '../field_components'; + +import { FieldsConfig, to, from, FormFieldsComponent, EDITOR_PX_HEIGHT } from './shared'; + +const { isJsonField, emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + + id: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldLabel', + { + defaultMessage: 'Stored script ID', + } + ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldHelpText', + { + defaultMessage: 'Stored script reference.', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.idRequiredError', { + defaultMessage: 'A value is required.', + }) + ), + }, + ], + }, + + source: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldLabel', { + defaultMessage: 'Source', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldHelpText', + { + defaultMessage: 'Script to be executed.', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.sourceRequiredError', { + defaultMessage: 'A value is required.', + }) + ), + }, + ], + }, + + /* Optional fields config */ + lang: { + type: FIELD_TYPES.TEXT, + deserializer: String, + serializer: from.undefinedIfValue('painless'), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.langFieldLabel', { + defaultMessage: 'Language (optional)', + }), + helpText: ( + {'painless'}, + }} + /> + ), + }, + + params: { + type: FIELD_TYPES.TEXT, + deserializer: to.jsonString, + serializer: from.optionalJson, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldLabel', { + defaultMessage: 'Parameters', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldHelpText', + { + defaultMessage: 'Script parameters.', + } + ), + validations: [ + { + validator: (value) => { + if (value.value) { + return isJsonField( + i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.scriptForm.processorInvalidJsonError', + { + defaultMessage: 'Invalid JSON', + } + ) + )(value); + } + }, + }, + ], + }, +}; + +export const Script: FormFieldsComponent = ({ initialFieldValues }) => { + const [showId, setShowId] = useState(() => !!initialFieldValues?.id); + return ( + <> + + setShowId((v) => !v)} + /> + + + {showId ? ( + + ) : ( + <> + + + + + )} + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx index 88cea620ae804..c282be35e5071 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx @@ -6,9 +6,10 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; import { - FieldConfig, FIELD_TYPES, fieldValidators, ToggleField, @@ -16,46 +17,68 @@ import { Field, } from '../../../../../../shared_imports'; -const { emptyField } = fieldValidators; +import { FieldsConfig, to, from } from './shared'; -const fieldConfig: FieldConfig = { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.fieldFieldLabel', { - defaultMessage: 'Field', - }), - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.fieldRequiredError', { - defaultMessage: 'A field value is required.', - }) - ), - }, - ], -}; +import { FieldNameField } from './common_fields/field_name_field'; -const valueConfig: FieldConfig = { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', { - defaultMessage: 'Value', - }), - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', { - defaultMessage: 'A value to set is required.', - }) - ), - }, - ], -}; +const { emptyField } = fieldValidators; -const overrideConfig: FieldConfig = { - defaultValue: false, - label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel', { - defaultMessage: 'Override', - }), - type: FIELD_TYPES.TOGGLE, +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + value: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', { + defaultMessage: 'Value', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText', { + defaultMessage: 'Value to be set for the field', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', { + defaultMessage: 'A value is required', + }) + ), + }, + ], + }, + /* Optional fields config */ + override: { + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + deserializer: to.booleanOrUndef, + serializer: from.undefinedIfValue(true), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel', { + defaultMessage: 'Override', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldHelpText', { + defaultMessage: 'If disabled, fields containing non-null values will not be updated.', + }), + }, + ignore_empty_value: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + deserializer: to.booleanOrUndef, + serializer: from.undefinedIfValue(false), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.setForm.ignoreEmptyValueFieldLabel', + { + defaultMessage: 'Ignore empty value', + } + ), + helpText: ( + {'value'}, + nullValue: {'null'}, + }} + /> + ), + }, }; /** @@ -64,11 +87,21 @@ const overrideConfig: FieldConfig = { export const SetProcessor: FunctionComponent = () => { return ( <> - + + + - + - + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx new file mode 100644 index 0000000000000..78128b3d54c75 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx @@ -0,0 +1,81 @@ +/* + * 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 React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; + +import { FIELD_TYPES, UseField, ComboBoxField } from '../../../../../../shared_imports'; + +import { FieldsConfig, to, from } from './shared'; + +import { FieldNameField } from './common_fields/field_name_field'; + +const userProperties: string[] = [ + 'username', + 'roles', + 'email', + 'full_name', + 'metadata', + 'api_key', + 'realm', + 'authentication_type', +]; + +const comboBoxOptions = userProperties.map((prop) => ({ label: prop })); +const helpTextValues = userProperties.join(', '); + +const fieldsConfig: FieldsConfig = { + /* Optional fields config */ + properties: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: to.arrayOfStrings, + serializer: from.optionalArrayOfStrings, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.propertiesFieldLabel', + { + defaultMessage: 'Properties (optional)', + } + ), + helpText: ( + [{helpTextValues}], + }} + /> + ), + }, +}; + +export const SetSecurityUser: FunctionComponent = () => { + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts index 84b308dd9cd7a..e45469e23e8a0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.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 { FunctionComponent } from 'react'; import * as rt from 'io-ts'; import { isRight } from 'fp-ts/lib/Either'; @@ -31,7 +31,8 @@ export function isArrayOfStrings(v: unknown): v is string[] { */ export const to = { booleanOrUndef: (v: unknown): boolean | undefined => (typeof v === 'boolean' ? v : undefined), - arrayOfStrings: (v: unknown): string[] => (isArrayOfStrings(v) ? v : []), + arrayOfStrings: (v: unknown): string[] => + isArrayOfStrings(v) ? v : typeof v === 'string' && v.length ? [v] : [], jsonString: (v: unknown) => (v ? JSON.stringify(v, null, 2) : '{}'), }; @@ -62,7 +63,17 @@ export const from = { } } }, - defaultBoolToUndef: (defaultBool: boolean) => (v: boolean) => (v === defaultBool ? undefined : v), + optionalArrayOfStrings: (v: string[]) => (v.length ? v : undefined), + undefinedIfValue: (value: any) => (v: boolean) => (v === value ? undefined : v), +}; + +export const EDITOR_PX_HEIGHT = { + extraSmall: 75, + small: 100, + medium: 200, + large: 300, }; export type FieldsConfig = Record; + +export type FormFieldsComponent = FunctionComponent<{ initialFieldValues?: Record }>; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx new file mode 100644 index 0000000000000..cdd0ff888accf --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx @@ -0,0 +1,71 @@ +/* + * 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 React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_TYPES, UseField, SelectField } from '../../../../../../shared_imports'; + +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { FieldsConfig, from } from './shared'; + +const fieldsConfig: FieldsConfig = { + /* Optional fields config */ + order: { + type: FIELD_TYPES.SELECT, + defaultValue: 'asc', + deserializer: (v) => (v === 'asc' || v === 'desc' ? v : 'asc'), + serializer: from.undefinedIfValue('asc'), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldLabel', { + defaultMessage: 'Order', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldHelpText', { + defaultMessage: 'Sort order to use', + }), + }, +}; + +export const Sort: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx new file mode 100644 index 0000000000000..b48ce74110b39 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx @@ -0,0 +1,91 @@ +/* + * 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 React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + ToggleField, +} from '../../../../../../shared_imports'; + +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldsConfig, to, from } from './shared'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + separator: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldLabel', { + defaultMessage: 'Separator', + }), + deserializer: String, + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldHelpText', + { + defaultMessage: 'Regex to match a separator', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.splitForm.separatorRequiredError', { + defaultMessage: 'A value is required.', + }) + ), + }, + ], + }, + /* Optional fields config */ + preserve_trailing: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + deserializer: to.booleanOrUndef, + serializer: from.undefinedIfValue(false), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldLabel', + { + defaultMessage: 'Preserve trailing', + } + ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldHelpText', + { defaultMessage: 'If enabled, preserve any trailing space.' } + ), + }, +}; + +export const Split: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx index 854c6632ab94a..fb400a0f75f51 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx @@ -5,7 +5,6 @@ */ import { i18n } from '@kbn/i18n'; -import { FunctionComponent } from 'react'; import { Append, @@ -28,13 +27,21 @@ import { Inference, Join, Json, + Kv, + Lowercase, + Pipeline, + Remove, + Rename, + Script, + SetProcessor, + SetSecurityUser, + Split, + Sort, + FormFieldsComponent, } from '../manage_processor_form/processors'; -// import { SetProcessor } from './processors/set'; -// import { Gsub } from './processors/gsub'; - interface FieldDescriptor { - FieldsComponent?: FunctionComponent; + FieldsComponent?: FormFieldsComponent; docLinkPath: string; /** * A sentence case label that can be displayed to users @@ -186,63 +193,70 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { }), }, kv: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Kv, docLinkPath: '/kv-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.kv', { defaultMessage: 'KV', }), }, lowercase: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Lowercase, docLinkPath: '/lowercase-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.lowercase', { defaultMessage: 'Lowercase', }), }, pipeline: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Pipeline, docLinkPath: '/pipeline-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.pipeline', { defaultMessage: 'Pipeline', }), }, remove: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Remove, docLinkPath: '/remove-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.remove', { defaultMessage: 'Remove', }), }, rename: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Rename, docLinkPath: '/rename-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.rename', { defaultMessage: 'Rename', }), }, script: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Script, docLinkPath: '/script-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.script', { defaultMessage: 'Script', }), }, + set: { + FieldsComponent: SetProcessor, + docLinkPath: '/set-processor.html', + label: i18n.translate('xpack.ingestPipelines.processors.label.set', { + defaultMessage: 'Set', + }), + }, set_security_user: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: SetSecurityUser, docLinkPath: '/ingest-node-set-security-user-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.setSecurityUser', { defaultMessage: 'Set security user', }), }, split: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Split, docLinkPath: '/split-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.split', { defaultMessage: 'Split', }), }, sort: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Sort, docLinkPath: '/sort-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.sort', { defaultMessage: 'Sort', @@ -276,15 +290,6 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { defaultMessage: 'User agent', }), }, - - // --- The below processor descriptors have components implemented --- - set: { - FieldsComponent: undefined, - docLinkPath: '/set-processor.html', - label: i18n.translate('xpack.ingestPipelines.processors.label.set', { - defaultMessage: 'Set', - }), - }, }; export type ProcessorType = keyof typeof mapProcessorTypeToDescriptor; diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 936db37f0c629..abdbdf2140400 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -62,6 +62,7 @@ export { RadioGroupField, NumericField, SelectField, + CheckBoxField, } from '../../../../src/plugins/es_ui_shared/static/forms/components'; export { diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 194f12cf9291b..0db456e0760ec 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -205,10 +205,10 @@ describe('Datatable Visualization', () => { }, frame, }).groups - ).toHaveLength(1); + ).toHaveLength(2); }); - it('allows all kinds of operations', () => { + it('allows only bucket operations one category', () => { const datasource = createMockDatasource('test'); const frame = mockFrame(); frame.datasourceLayers = { first: datasource.publicAPIMock }; @@ -232,6 +232,40 @@ describe('Datatable Visualization', () => { expect(filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(true); expect(filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual(true); expect(filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false })).toEqual( + false + ); + expect(filterOperations({ ...baseOperation, dataType: 'number', isBucketed: false })).toEqual( + false + ); + }); + + it('allows only metric operations in one category', () => { + const datasource = createMockDatasource('test'); + const frame = mockFrame(); + frame.datasourceLayers = { first: datasource.publicAPIMock }; + + const filterOperations = datatableVisualization.getConfiguration({ + layerId: 'first', + state: { + layers: [{ layerId: 'first', columns: [] }], + }, + frame, + }).groups[1].filterOperations; + + const baseOperation: Operation = { + dataType: 'string', + isBucketed: true, + label: '', + }; + expect(filterOperations({ ...baseOperation })).toEqual(false); + expect(filterOperations({ ...baseOperation, dataType: 'number' })).toEqual(false); + expect(filterOperations({ ...baseOperation, dataType: 'date' })).toEqual(false); + expect(filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(false); + expect(filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual(false); + expect(filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false })).toEqual( + true + ); + expect(filterOperations({ ...baseOperation, dataType: 'number', isBucketed: false })).toEqual( true ); }); @@ -248,7 +282,7 @@ describe('Datatable Visualization', () => { layerId: 'a', state: { layers: [layer] }, frame, - }).groups[0].accessors + }).groups[1].accessors ).toEqual(['c', 'b']); }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 5aff4e14b17f2..836ffcb15cfa1 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -143,15 +143,29 @@ export const datatableVisualization: Visualization groups: [ { groupId: 'columns', - groupLabel: i18n.translate('xpack.lens.datatable.columns', { - defaultMessage: 'Columns', + groupLabel: i18n.translate('xpack.lens.datatable.breakdown', { + defaultMessage: 'Break down by', }), layerId: state.layers[0].layerId, - accessors: sortedColumns, + accessors: sortedColumns.filter((c) => datasource.getOperationForColumnId(c)?.isBucketed), supportsMoreColumns: true, - filterOperations: () => true, + filterOperations: (op) => op.isBucketed, dataTestSubj: 'lnsDatatable_column', }, + { + groupId: 'metrics', + groupLabel: i18n.translate('xpack.lens.datatable.metrics', { + defaultMessage: 'Metrics', + }), + layerId: state.layers[0].layerId, + accessors: sortedColumns.filter( + (c) => !datasource.getOperationForColumnId(c)?.isBucketed + ), + supportsMoreColumns: true, + filterOperations: (op) => !op.isBucketed, + required: true, + dataTestSubj: 'lnsDatatable_metrics', + }, ], }; }, diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap index d18a2db614f55..3581151dd5f76 100644 --- a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap +++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap @@ -9,6 +9,20 @@ exports[`DragDrop droppable is reflected in the className 1`] = `
    `; +exports[`DragDrop items that have droppable=false get special styling when another item is dragged 1`] = ` +
    + Hello! +
    +`; + exports[`DragDrop renders if nothing is being dragged 1`] = `
    { const value = {}; const component = mount( - + Hello! @@ -127,4 +127,63 @@ describe('DragDrop', () => { expect(component).toMatchSnapshot(); }); + + test('items that have droppable=false get special styling when another item is dragged', () => { + const component = mount( + {}}> + + Ignored + + {}} droppable={false}> + Hello! + + + ); + + expect(component.find('[data-test-subj="lnsDragDrop"]').at(1)).toMatchSnapshot(); + }); + + test('additional styles are reflected in the className until drop', () => { + let dragging: string | undefined; + const getAdditionalClasses = jest.fn().mockReturnValue('additional'); + const component = mount( + { + dragging = 'hello'; + }} + > + + Ignored + + {}} + droppable + getAdditionalClassesOnEnter={getAdditionalClasses} + > + Hello! + + + ); + + const dataTransfer = { + setData: jest.fn(), + getData: jest.fn(), + }; + component + .find('[data-test-subj="lnsDragDrop"]') + .first() + .simulate('dragstart', { dataTransfer }); + jest.runAllTimers(); + + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover'); + expect(component.find('.additional')).toHaveLength(1); + + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave'); + expect(component.find('.additional')).toHaveLength(0); + + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover'); + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('drop'); + expect(component.find('.additional')).toHaveLength(0); + }); }); diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 5a0fc3b3839f7..85bdd24bd4f80 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -49,6 +49,11 @@ interface BaseProps { */ droppable?: boolean; + /** + * Additional class names to apply when another element is over the drop target + */ + getAdditionalClassesOnEnter?: () => string; + /** * The optional test subject associated with this DOM element. */ @@ -97,6 +102,12 @@ export const DragDrop = (props: Props) => { {...props} dragging={droppable ? dragging : undefined} isDragging={!!(draggable && value === dragging)} + isNotDroppable={ + // If the configuration has provided a droppable flag, but this particular item is not + // droppable, then it should be less prominent. Ignores items that are both + // draggable and drop targets + droppable === false && Boolean(dragging) && value !== dragging + } setDragging={setDragging} /> ); @@ -107,9 +118,13 @@ const DragDropInner = React.memo(function DragDropInner( dragging: unknown; setDragging: (dragging: unknown) => void; isDragging: boolean; + isNotDroppable: boolean; } ) { - const [state, setState] = useState({ isActive: false }); + const [state, setState] = useState({ + isActive: false, + dragEnterClassNames: '', + }); const { className, onDrop, @@ -120,13 +135,20 @@ const DragDropInner = React.memo(function DragDropInner( dragging, setDragging, isDragging, + isNotDroppable, } = props; - const classes = classNames('lnsDragDrop', className, { - 'lnsDragDrop-isDropTarget': droppable, - 'lnsDragDrop-isActiveDropTarget': droppable && state.isActive, - 'lnsDragDrop-isDragging': isDragging, - }); + const classes = classNames( + 'lnsDragDrop', + className, + { + 'lnsDragDrop-isDropTarget': droppable, + 'lnsDragDrop-isActiveDropTarget': droppable && state.isActive, + 'lnsDragDrop-isDragging': isDragging, + 'lnsDragDrop-isNotDroppable': isNotDroppable, + }, + state.dragEnterClassNames + ); const dragStart = (e: DroppableEvent) => { // Setting stopPropgagation causes Chrome failures, so @@ -159,19 +181,25 @@ const DragDropInner = React.memo(function DragDropInner( // An optimization to prevent a bunch of React churn. if (!state.isActive) { - setState({ ...state, isActive: true }); + setState({ + ...state, + isActive: true, + dragEnterClassNames: props.getAdditionalClassesOnEnter + ? props.getAdditionalClassesOnEnter() + : '', + }); } }; const dragLeave = () => { - setState({ ...state, isActive: false }); + setState({ ...state, isActive: false, dragEnterClassNames: '' }); }; const drop = (e: DroppableEvent) => { e.preventDefault(); e.stopPropagation(); - setState({ ...state, isActive: false }); + setState({ ...state, isActive: false, dragEnterClassNames: '' }); setDragging(undefined); if (onDrop && droppable) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss index 4e13fd95d1961..62bc6d7ed7cc8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss @@ -27,6 +27,14 @@ overflow: hidden; } +.lnsLayerPanel__dimension-isHidden { + opacity: 0; +} + +.lnsLayerPanel__dimension-isReplacing { + text-decoration: line-through; +} + .lnsLayerPanel__triggerLink { padding: $euiSizeS; width: 100%; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index b3ad03b71770c..85dbee6de524f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -12,6 +12,7 @@ import { createMockDatasource, DatasourceMock, } from '../../mocks'; +import { ChildDragDropProvider } from '../../../drag_drop'; import { EuiFormRow, EuiPopover } from '@elastic/eui'; import { mount } from 'enzyme'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -272,6 +273,7 @@ describe('LayerPanel', () => { expect(component.find(EuiPopover).prop('isOpen')).toBe(true); }); + it('should close the popover when the active visualization changes', () => { /** * The ID generation system for new dimensions has been messy before, so @@ -324,4 +326,151 @@ describe('LayerPanel', () => { expect(component.find(EuiPopover).prop('isOpen')).toBe(false); }); }); + + // This test is more like an integration test, since the layer panel owns all + // the coordination between drag and drop + describe('drag and drop behavior', () => { + it('should determine if the datasource supports dropping of a field onto empty dimension', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + mockDatasource.canHandleDrop.mockReturnValue(true); + + const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a' }; + + const component = mountWithIntl( + + + + ); + + expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dragDropContext: expect.objectContaining({ + dragging: draggingField, + }), + }) + ); + + component.find('DragDrop[data-test-subj="lnsGroup"]').first().simulate('drop'); + + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dragDropContext: expect.objectContaining({ + dragging: draggingField, + }), + }) + ); + }); + + it('should allow drag to move between groups', () => { + (generateId as jest.Mock).mockReturnValue(`newid`); + + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['a'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroupA', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: ['b'], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroupB', + }, + ], + }); + + mockDatasource.canHandleDrop.mockReturnValue(true); + + const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a' }; + + const component = mountWithIntl( + + + + ); + + expect(mockDatasource.canHandleDrop).toHaveBeenCalledTimes(2); + expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dragDropContext: expect.objectContaining({ + dragging: draggingOperation, + }), + }) + ); + + // Simulate drop on the pre-populated dimension + component.find('DragDrop[data-test-subj="lnsGroupB"]').at(0).simulate('drop'); + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + columnId: 'b', + dragDropContext: expect.objectContaining({ + dragging: draggingOperation, + }), + }) + ); + + // Simulate drop on the empty dimension + component.find('DragDrop[data-test-subj="lnsGroupB"]').at(1).simulate('drop'); + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + columnId: 'newid', + dragDropContext: expect.objectContaining({ + dragging: draggingOperation, + }), + }) + ); + }); + + it('should prevent dropping in the same group', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['a', 'b'], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a' }; + + const component = mountWithIntl( + + + + ); + + expect(mockDatasource.canHandleDrop).not.toHaveBeenCalled(); + + component.find('DragDrop[data-test-subj="lnsGroup"]').at(0).simulate('drop'); + expect(mockDatasource.onDrop).not.toHaveBeenCalled(); + + component.find('DragDrop[data-test-subj="lnsGroup"]').at(1).simulate('drop'); + expect(mockDatasource.onDrop).not.toHaveBeenCalled(); + + component.find('DragDrop[data-test-subj="lnsGroup"]').at(2).simulate('drop'); + expect(mockDatasource.onDrop).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index b2804cfddba58..b45dd13bfa4fd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -17,8 +17,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import classNames from 'classnames'; import { NativeRenderer } from '../../../native_renderer'; -import { StateSetter } from '../../../types'; +import { StateSetter, isDraggedOperation } from '../../../types'; import { DragContext, DragDrop, ChildDragDropProvider } from '../../../drag_drop'; import { LayerSettings } from './layer_settings'; import { trackUiEvent } from '../../../lens_ui_telemetry'; @@ -154,6 +155,7 @@ export function LayerPanel( {groups.map((group, index) => { const newId = generateId(); const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( { + // If we are dragging another column, add an indication that the behavior will be a replacement' + if ( + isDraggedOperation(dragDropContext.dragging) && + group.groupId !== dragDropContext.dragging.groupId + ) { + return 'lnsLayerPanel__dimension-isReplacing'; + } + return ''; + }} data-test-subj={group.dataTestSubj} + draggable={true} + value={{ columnId: accessor, groupId: group.groupId, layerId }} + label={group.groupLabel} droppable={ - dragDropContext.dragging && + Boolean(dragDropContext.dragging) && + // Verify that the dragged item is not coming from the same group + // since this would be a reorder + (!isDraggedOperation(dragDropContext.dragging) || + dragDropContext.dragging.groupId !== group.groupId) && layerDatasource.canHandleDrop({ ...layerDatasourceDropProps, columnId: accessor, @@ -226,12 +250,22 @@ export function LayerPanel( }) } onDrop={(droppedItem) => { - layerDatasource.onDrop({ + const dropResult = layerDatasource.onDrop({ ...layerDatasourceDropProps, droppedItem, columnId: accessor, filterOperations: group.filterOperations, }); + if (typeof dropResult === 'object') { + // When a column is moved, we delete the reference to the old + props.updateVisualization( + activeVisualization.removeDimension({ + layerId, + columnId: dropResult.deleted, + prevState: props.visualizationState, + }) + ); + } }} > { - const dropSuccess = layerDatasource.onDrop({ + const dropResult = layerDatasource.onDrop({ ...layerDatasourceDropProps, droppedItem, columnId: newId, filterOperations: group.filterOperations, }); - if (dropSuccess) { + if (dropResult) { props.updateVisualization( activeVisualization.setDimension({ layerId, @@ -338,6 +376,17 @@ export function LayerPanel( prevState: props.visualizationState, }) ); + + if (typeof dropResult === 'object') { + // When a column is moved, we delete the reference to the old + props.updateVisualization( + activeVisualization.removeDimension({ + layerId, + columnId: dropResult.deleted, + prevState: props.visualizationState, + }) + ); + } } }} > diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 3ee109376d975..f184d5628ab1c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -1378,6 +1378,66 @@ describe('IndexPatternDimensionEditorPanel', () => { ).toBe(false); }); + it('is droppable if the dragged column is compatible', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { + columnId: 'col1', + groupId: 'a', + layerId: 'myLayer', + }, + }, + state: dragDropState(), + columnId: 'col2', + filterOperations: (op: OperationMetadata) => true, + layerId: 'myLayer', + }) + ).toBe(true); + }); + + it('is not droppable if the dragged column is the same as the current column', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { + columnId: 'col1', + groupId: 'a', + layerId: 'myLayer', + }, + }, + state: dragDropState(), + columnId: 'col1', + filterOperations: (op: OperationMetadata) => true, + layerId: 'myLayer', + }) + ).toBe(false); + }); + + it('is not droppable if the dragged column is incompatible', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { + columnId: 'col1', + groupId: 'a', + layerId: 'myLayer', + }, + }, + state: dragDropState(), + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(false); + }); + it('appends the dropped column when a field is dropped', () => { const dragging = { field: { type: 'number', name: 'bar', aggregatable: true }, @@ -1526,5 +1586,109 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }); }); + + it('updates the column id when moving an operation to an empty dimension', () => { + const dragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'myLayer', + }; + const testState = dragDropState(); + + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => true, + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col2'], + columns: { + col2: testState.layers.myLayer.columns.col1, + }, + }, + }, + }); + }); + + it('replaces an operation when moving to a populated dimension', () => { + const dragging = { + columnId: 'col2', + groupId: 'a', + layerId: 'myLayer', + }; + const testState = dragDropState(); + testState.layers.myLayer = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.myLayer.columns.col1, + + col2: { + label: 'Top values of src', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col3' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'src', + }, + col3: { + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + sourceField: 'Records', + }, + }, + }; + + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col1', + filterOperations: (op: OperationMetadata) => true, + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col1', 'col3'], + columns: { + col1: testState.layers.myLayer.columns.col2, + col3: testState.layers.myLayer.columns.col3, + }, + }, + }, + }); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 1e8f73b19a3b0..1fbbefd8f1117 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -15,6 +15,7 @@ import { DatasourceDimensionEditorProps, DatasourceDimensionDropProps, DatasourceDimensionDropHandlerProps, + isDraggedOperation, } from '../../types'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { IndexPatternColumn, OperationType } from '../indexpattern'; @@ -99,16 +100,25 @@ export function canHandleDrop(props: DatasourceDimensionDropProps -): boolean { +export function onDrop(props: DatasourceDimensionDropHandlerProps) { const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); const droppedItem = props.droppedItem; @@ -116,6 +126,42 @@ export function onDrop( return Boolean(operationFieldSupportMatrix.operationByField[field.name]); } + if (isDraggedOperation(droppedItem) && droppedItem.layerId === props.layerId) { + const layer = props.state.layers[props.layerId]; + const op = { ...layer.columns[droppedItem.columnId] }; + if (!props.filterOperations(op)) { + return false; + } + + const newColumns = { ...layer.columns }; + delete newColumns[droppedItem.columnId]; + newColumns[props.columnId] = op; + + const newColumnOrder = [...layer.columnOrder]; + const oldIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId); + const newIndex = newColumnOrder.findIndex((c) => c === props.columnId); + + if (newIndex === -1) { + newColumnOrder[oldIndex] = props.columnId; + } else { + newColumnOrder.splice(oldIndex, 1); + } + + // Time to replace + props.setState({ + ...props.state, + layers: { + ...props.state.layers, + [props.layerId]: { + ...layer, + columnOrder: newColumnOrder, + columns: newColumns, + }, + }, + }); + return { deleted: droppedItem.columnId }; + } + if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { // TODO: What do we do if we couldn't find a column? return false; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index 0cd92fd96c952..374dbe77b4ca3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DataType } from '../types'; import { DraggedField } from './indexpattern'; import { BaseIndexPatternColumn, FieldBasedIndexPatternColumn, } from './operations/definitions/column_types'; -import { DataType } from '../types'; /** * Normalizes the specified operation type. (e.g. document operations diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 729daed7223fe..d8b77afdfe004 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -157,7 +157,7 @@ export interface Datasource { renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; canHandleDrop: (props: DatasourceDimensionDropProps) => boolean; - onDrop: (props: DatasourceDimensionDropHandlerProps) => boolean; + onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; toExpression: (state: T, layerId: string) => Ast | string | null; @@ -230,6 +230,22 @@ export interface DatasourceLayerPanelProps { setState: StateSetter; } +export interface DraggedOperation { + layerId: string; + groupId: string; + columnId: string; +} + +export function isDraggedOperation( + operationCandidate: unknown +): operationCandidate is DraggedOperation { + return ( + typeof operationCandidate === 'object' && + operationCandidate !== null && + 'columnId' in operationCandidate + ); +} + export type DatasourceDimensionDropProps = SharedDimensionProps & { layerId: string; columnId: string; diff --git a/x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts b/x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts index fc9d4f9381151..b2390ea35c140 100644 --- a/x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts +++ b/x-pack/plugins/licensing/public/services/feature_usage_service.mock.ts @@ -15,6 +15,8 @@ const createSetupMock = (): jest.Mocked => { register: jest.fn(), }; + mock.register.mockImplementation(() => Promise.resolve()); + return mock; }; @@ -23,6 +25,8 @@ const createStartMock = (): jest.Mocked => { notifyUsage: jest.fn(), }; + mock.notifyUsage.mockImplementation(() => Promise.resolve()); + return mock; }; diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts index fad8ecc86277b..ec3871b673888 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts @@ -16,6 +16,8 @@ import { EsDataTypeRangeTerm, EsDataTypeSingle, EsDataTypeUnion, + ExceptionListTypeEnum, + OperatorEnum, Type, esDataTypeGeoPoint, esDataTypeGeoPointRange, @@ -25,60 +27,10 @@ import { esDataTypeUnion, exceptionListType, operator, - operator_type as operatorType, type, } from './schemas'; describe('Common schemas', () => { - describe('operatorType', () => { - test('it should validate for "match"', () => { - const payload = 'match'; - const decoded = operatorType.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate for "match_any"', () => { - const payload = 'match_any'; - const decoded = operatorType.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate for "list"', () => { - const payload = 'list'; - const decoded = operatorType.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should validate for "exists"', () => { - const payload = 'exists'; - const decoded = operatorType.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should contain 4 keys', () => { - // Might seem like a weird test, but its meant to - // ensure that if operatorType is updated, you - // also update the OperatorTypeEnum, a workaround - // for io-ts not yet supporting enums - // https://github.com/gcanti/io-ts/issues/67 - const keys = Object.keys(operatorType.keys); - - expect(keys.length).toEqual(4); - }); - }); - describe('operator', () => { test('it should validate for "included"', () => { const payload = 'included'; @@ -98,15 +50,16 @@ describe('Common schemas', () => { expect(message.schema).toEqual(payload); }); - test('it should contain 2 keys', () => { + test('it should contain same amount of keys as enum', () => { // Might seem like a weird test, but its meant to // ensure that if operator is updated, you // also update the operatorEnum, a workaround // for io-ts not yet supporting enums // https://github.com/gcanti/io-ts/issues/67 - const keys = Object.keys(operator.keys); + const keys = Object.keys(operator.keys).sort().join(',').toLowerCase(); + const enumKeys = Object.keys(OperatorEnum).sort().join(',').toLowerCase(); - expect(keys.length).toEqual(2); + expect(keys).toEqual(enumKeys); }); }); @@ -129,15 +82,16 @@ describe('Common schemas', () => { expect(message.schema).toEqual(payload); }); - test('it should contain 2 keys', () => { + test('it should contain same amount of keys as enum', () => { // Might seem like a weird test, but its meant to // ensure that if exceptionListType is updated, you // also update the ExceptionListTypeEnum, a workaround // for io-ts not yet supporting enums // https://github.com/gcanti/io-ts/issues/67 - const keys = Object.keys(exceptionListType.keys); + const keys = Object.keys(exceptionListType.keys).sort().join(',').toLowerCase(); + const enumKeys = Object.keys(ExceptionListTypeEnum).sort().join(',').toLowerCase(); - expect(keys.length).toEqual(2); + expect(keys).toEqual(enumKeys); }); }); diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index 1556ef5a5dab9..37da5fbcd1a1b 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -282,13 +282,6 @@ export enum OperatorEnum { EXCLUDED = 'excluded', } -export const operator_type = t.keyof({ - exists: null, - list: null, - match: null, - match_any: null, -}); -export type OperatorType = t.TypeOf; export enum OperatorTypeEnum { NESTED = 'nested', MATCH = 'match', diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 1f6c65919b063..361837bdef229 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -25,7 +25,6 @@ export { NamespaceType, Operator, OperatorEnum, - OperatorType, OperatorTypeEnum, ExceptionListTypeEnum, comment, diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 7e79b212f69cb..0c31015fc9f5e 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -375,6 +375,8 @@ describe('Exceptions Lists API', () => { namespace_type: 'single,single', page: '1', per_page: '20', + sort_field: 'exception-list.created_at', + sort_order: 'desc', }, signal: abortCtrl.signal, }); @@ -406,6 +408,8 @@ describe('Exceptions Lists API', () => { namespace_type: 'single', page: '1', per_page: '20', + sort_field: 'exception-list.created_at', + sort_order: 'desc', }, signal: abortCtrl.signal, }); @@ -437,6 +441,8 @@ describe('Exceptions Lists API', () => { namespace_type: 'agnostic', page: '1', per_page: '20', + sort_field: 'exception-list.created_at', + sort_order: 'desc', }, signal: abortCtrl.signal, }); @@ -468,6 +474,8 @@ describe('Exceptions Lists API', () => { namespace_type: 'agnostic', page: '1', per_page: '20', + sort_field: 'exception-list.created_at', + sort_order: 'desc', }, signal: abortCtrl.signal, }); @@ -500,6 +508,8 @@ describe('Exceptions Lists API', () => { namespace_type: 'agnostic', page: '1', per_page: '20', + sort_field: 'exception-list.created_at', + sort_order: 'desc', }, signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index 203c84b2943fd..824a25296260f 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -288,6 +288,8 @@ export const fetchExceptionListsItemsByListIds = async ({ namespace_type: namespaceTypes.join(','), page: pagination.page ? `${pagination.page}` : '1', per_page: pagination.perPage ? `${pagination.perPage}` : '20', + sort_field: 'exception-list.created_at', + sort_order: 'desc', ...(filters.trim() !== '' ? { filter: filters } : {}), }; const [validatedRequest, errorsRequest] = validate(query, findExceptionListItemSchema); diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js index 56b830e9ff098..d51ca46fd98ff 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js @@ -281,7 +281,7 @@ export class AbstractESSource extends AbstractVectorSource { return null; } - return fieldFromIndexPattern.format.getConverterFor('text'); + return indexPattern.getFormatterForField(fieldFromIndexPattern).getConverterFor('text'); } async loadStylePropsMeta( diff --git a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts index 516ad25f933f6..348cd5e552258 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts @@ -10,8 +10,8 @@ import { IField } from '../fields/field'; import { esFilters, Filter, - IFieldType, IndexPattern, + IndexPatternField, } from '../../../../../../src/plugins/data/public'; export class ESTooltipProperty implements ITooltipProperty { @@ -37,7 +37,7 @@ export class ESTooltipProperty implements ITooltipProperty { return this._tooltipProperty.getRawValue(); } - _getIndexPatternField(): IFieldType | undefined { + _getIndexPatternField(): IndexPatternField | undefined { return this._indexPattern.fields.getByName(this._field.getRootName()); } @@ -56,10 +56,11 @@ export class ESTooltipProperty implements ITooltipProperty { } } - const htmlConverter = indexPatternField.format.getConverterFor('html'); + const formatter = this._indexPattern.getFormatterForField(indexPatternField); + const htmlConverter = formatter.getConverterFor('html'); return htmlConverter ? htmlConverter(this.getRawValue()) - : indexPatternField.format.convert(this.getRawValue()); + : formatter.convert(this.getRawValue()); } isFilterable(): boolean { diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js index 6c7e141c2ab5a..66af92c7a687b 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js @@ -25,5 +25,5 @@ export const getMapsSavedObjectLoader = _.once(function () { }; const SavedGisMap = createSavedGisMapClass(services); - return new SavedObjectLoader(SavedGisMap, getSavedObjectsClient(), getCoreChrome()); + return new SavedObjectLoader(SavedGisMap, getSavedObjectsClient()); }); diff --git a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js index e9229883d708d..8fe6866cd7834 100644 --- a/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js +++ b/x-pack/plugins/maps/public/routing/routes/list/maps_list_view.js @@ -12,6 +12,7 @@ import { getUiSettings, getToasts, getCoreChrome, + getNavigateToApp, } from '../../../kibana_services'; import { EuiTitle, @@ -32,11 +33,18 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { addHelpMenuToAppChrome } from '../../../help_menu_util'; -import { Link } from 'react-router-dom'; import { goToSpecifiedPath } from '../../maps_router'; +import { APP_ID, MAP_PATH } from '../../../../common/constants'; export const EMPTY_FILTER = ''; +function navigateToNewMap() { + const navigateToApp = getNavigateToApp(); + navigateToApp(APP_ID, { + path: MAP_PATH, + }); +} + export class MapsListView extends React.Component { state = { hasInitialFetchReturned: false, @@ -388,14 +396,12 @@ export class MapsListView extends React.Component { let createButton; if (!this.state.readOnly) { createButton = ( - - - - - + + + ); } return ( diff --git a/x-pack/plugins/ml/common/constants/ml_url_generator.ts b/x-pack/plugins/ml/common/constants/ml_url_generator.ts new file mode 100644 index 0000000000000..44f33aa329e7a --- /dev/null +++ b/x-pack/plugins/ml/common/constants/ml_url_generator.ts @@ -0,0 +1,38 @@ +/* + * 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 const ML_APP_URL_GENERATOR = 'ML_APP_URL_GENERATOR'; + +export const ML_PAGES = { + ANOMALY_DETECTION_JOBS_MANAGE: 'jobs', + ANOMALY_EXPLORER: 'explorer', + SINGLE_METRIC_VIEWER: 'timeseriesexplorer', + DATA_FRAME_ANALYTICS_JOBS_MANAGE: 'data_frame_analytics', + DATA_FRAME_ANALYTICS_EXPLORATION: 'data_frame_analytics/exploration', + /** + * Page: Data Visualizer + */ + DATA_VISUALIZER: 'datavisualizer', + /** + * Page: Data Visualizer + * Open data visualizer by selecting a Kibana index pattern or saved search + */ + DATA_VISUALIZER_INDEX_SELECT: 'datavisualizer_index_select', + /** + * Page: Data Visualizer + * Open data visualizer by importing data from a log file + */ + DATA_VISUALIZER_FILE: 'filedatavisualizer', + /** + * Page: Data Visualizer + * Open index data visualizer viewer page + */ + DATA_VISUALIZER_INDEX_VIEWER: 'jobs/new_job/datavisualizer', + ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: `jobs/new_job/step/job_type`, + SETTINGS: 'settings', + CALENDARS_MANAGE: 'settings/calendars_list', + FILTER_LISTS_MANAGE: 'settings/filter_lists', +} as const; diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts new file mode 100644 index 0000000000000..234be8b6faf90 --- /dev/null +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -0,0 +1,187 @@ +/* + * 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 { RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common/query'; +import { JobId } from '../../../reporting/common/types'; +import { ML_PAGES } from '../constants/ml_url_generator'; + +type OptionalPageState = object | undefined; + +export type MLPageState = PageState extends OptionalPageState + ? { page: PageType; pageState?: PageState } + : PageState extends object + ? { page: PageType; pageState: PageState } + : { page: PageType }; + +export const ANALYSIS_CONFIG_TYPE = { + OUTLIER_DETECTION: 'outlier_detection', + REGRESSION: 'regression', + CLASSIFICATION: 'classification', +} as const; + +type DataFrameAnalyticsType = typeof ANALYSIS_CONFIG_TYPE[keyof typeof ANALYSIS_CONFIG_TYPE]; + +export interface MlCommonGlobalState { + time?: TimeRange; +} +export interface MlCommonAppState { + [key: string]: any; +} + +export interface MlIndexBasedSearchState { + index?: string; + savedSearchId?: string; +} + +export interface MlGenericUrlPageState extends MlIndexBasedSearchState { + globalState?: MlCommonGlobalState; + appState?: MlCommonAppState; + [key: string]: any; +} + +export interface MlGenericUrlState { + page: + | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER + | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE; + pageState: MlGenericUrlPageState; +} + +export interface AnomalyDetectionQueryState { + jobId?: JobId; + groupIds?: string[]; +} + +export type AnomalyDetectionUrlState = MLPageState< + typeof ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + AnomalyDetectionQueryState | undefined +>; +export interface ExplorerAppState { + mlExplorerSwimlane: { + selectedType?: string; + selectedLanes?: string[]; + selectedTimes?: number[]; + showTopFieldValues?: boolean; + viewByFieldName?: string; + viewByPerPage?: number; + viewByFromPage?: number; + }; + mlExplorerFilter: { + influencersFilterQuery?: unknown; + filterActive?: boolean; + filteredFields?: string[]; + queryString?: string; + }; + query?: any; +} +export interface ExplorerGlobalState { + ml: { jobIds: JobId[] }; + time?: TimeRange; + refreshInterval?: RefreshInterval; +} + +export interface ExplorerUrlPageState { + /** + * Job IDs + */ + jobIds: JobId[]; + /** + * Optionally set the time range in the time picker. + */ + timeRange?: TimeRange; + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval; + /** + * Optionally set the query. + */ + query?: any; + /** + * Optional state for the swim lane + */ + mlExplorerSwimlane?: ExplorerAppState['mlExplorerSwimlane']; + mlExplorerFilter?: ExplorerAppState['mlExplorerFilter']; +} + +export type ExplorerUrlState = MLPageState; + +export interface TimeSeriesExplorerGlobalState { + ml: { + jobIds: JobId[]; + }; + time?: TimeRange; + refreshInterval?: RefreshInterval; +} + +export interface TimeSeriesExplorerAppState { + zoom?: { + from?: string; + to?: string; + }; + mlTimeSeriesExplorer?: { + detectorIndex?: number; + entities?: Record; + }; + query?: any; +} + +export interface TimeSeriesExplorerPageState + extends Pick, + Pick { + jobIds: JobId[]; + timeRange?: TimeRange; + detectorIndex?: number; + entities?: Record; +} + +export type TimeSeriesExplorerUrlState = MLPageState< + typeof ML_PAGES.SINGLE_METRIC_VIEWER, + TimeSeriesExplorerPageState +>; + +export interface DataFrameAnalyticsQueryState { + jobId?: JobId | JobId[]; + groupIds?: string[]; +} + +export type DataFrameAnalyticsUrlState = MLPageState< + typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + DataFrameAnalyticsQueryState | undefined +>; + +export interface DataVisualizerUrlState { + page: + | typeof ML_PAGES.DATA_VISUALIZER + | typeof ML_PAGES.DATA_VISUALIZER_FILE + | typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT; +} + +export interface DataFrameAnalyticsExplorationQueryState { + ml: { + jobId: JobId; + analysisType: DataFrameAnalyticsType; + }; +} + +export type DataFrameAnalyticsExplorationUrlState = MLPageState< + typeof ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + { + jobId: JobId; + analysisType: DataFrameAnalyticsType; + } +>; + +/** + * Union type of ML URL state based on page + */ +export type MlUrlGeneratorState = + | AnomalyDetectionUrlState + | ExplorerUrlState + | TimeSeriesExplorerUrlState + | DataFrameAnalyticsUrlState + | DataFrameAnalyticsExplorationUrlState + | DataVisualizerUrlState + | MlGenericUrlState; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 5a7d2a9c3ddaa..fdeab0c49e32b 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -390,6 +390,7 @@ class LinksMenuUI extends Component { defaultMessage: 'Select action for anomaly at {time}', values: { time: formatHumanReadableDateTimeSeconds(anomaly.time) }, })} + data-test-subj="mlAnomaliesListRowActionsButton" /> ); @@ -404,6 +405,7 @@ class LinksMenuUI extends Component { this.closePopover(); this.openCustomUrl(customUrl); }} + data-test-subj={`mlAnomaliesListRowActionCustomUrlButton_${index}`} > {customUrl.url_name} @@ -420,6 +422,7 @@ class LinksMenuUI extends Component { this.closePopover(); this.viewSeries(); }} + data-test-subj="mlAnomaliesListRowActionViewSeriesButton" > - + ); } diff --git a/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js b/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js index a710ce18856f0..1f03dbe134756 100644 --- a/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js +++ b/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js @@ -29,7 +29,7 @@ export const RecognizedResult = ({ config, indexPattern, savedSearch }) => { return ( { return ( - + { setItemSelected(item, e.target.checked); }} + data-test-subj={`mlGridItemCheckbox`} /> ); diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts index 8a43ae12deb21..44979a1b9c60a 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/index.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/index.ts @@ -9,3 +9,4 @@ export { useNavigateToPath, NavigateToPath } from './use_navigate_to_path'; export { useUiSettings } from './use_ui_settings_context'; export { useTimefilter } from './use_timefilter'; export { useNotifications } from './use_notifications_context'; +export { useMlUrlGenerator } from './use_create_url'; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts new file mode 100644 index 0000000000000..48385ad3ae6a8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.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. + */ + +import { useMlKibana } from './kibana_context'; +import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator'; + +export const useMlUrlGenerator = () => { + const { + services: { + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = useMlKibana(); + + return getUrlGenerator(ML_APP_URL_GENERATOR); +}; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts index f2db970bf5057..96d41be03a142 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts @@ -21,14 +21,19 @@ export const useNavigateToPath = () => { const location = useLocation(); - return useMemo( - () => (path: string | undefined, preserveSearch = false) => { - navigateToUrl( - getUrlForApp(PLUGIN_ID, { - path: `${path}${preserveSearch === true ? location.search : ''}`, - }) - ); - }, - [location] - ); + return useMemo(() => { + return (path: string | undefined, preserveSearch = false) => { + if (path === undefined) return; + const modifiedPath = `${path}${preserveSearch === true ? location.search : ''}`; + /** + * Handle urls generated by MlUrlGenerator where '/app/ml' is automatically prepended + */ + const url = modifiedPath.includes('/app/ml') + ? modifiedPath + : getUrlForApp(PLUGIN_ID, { + path: modifiedPath, + }); + navigateToUrl(url); + }; + }, [location]); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx index babb557105270..0560c3150a424 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/back_to_list_panel/back_to_list_panel.tsx @@ -7,13 +7,20 @@ import React, { FC, Fragment } from 'react'; import { EuiCard, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useNavigateToPath } from '../../../../../contexts/kibana'; +import { useMlKibana, useMlUrlGenerator } from '../../../../../contexts/kibana'; +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; export const BackToListPanel: FC = () => { - const navigateToPath = useNavigateToPath(); + const urlGenerator = useMlUrlGenerator(); + const { + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); const redirectToAnalyticsManagementPage = async () => { - await navigateToPath('/data_frame_analytics'); + const url = await urlGenerator.createUrl({ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE }); + await navigateToUrl(url); }; return ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/view_results_panel/view_results_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/view_results_panel/view_results_panel.tsx index 23a16ba84ef92..b832bfb2549c6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/view_results_panel/view_results_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/view_results_panel/view_results_panel.tsx @@ -7,20 +7,27 @@ import React, { FC, Fragment } from 'react'; import { EuiCard, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useNavigateToPath } from '../../../../../contexts/kibana'; -import { getResultsUrl } from '../../../analytics_management/components/analytics_list/common'; +import { useMlUrlGenerator } from '../../../../../contexts/kibana'; import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; - +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; +import { useNavigateToPath } from '../../../../../contexts/kibana'; interface Props { jobId: string; analysisType: ANALYSIS_CONFIG_TYPE; } export const ViewResultsPanel: FC = ({ jobId, analysisType }) => { + const urlGenerator = useMlUrlGenerator(); const navigateToPath = useNavigateToPath(); - const redirectToAnalyticsManagementPage = async () => { - const path = getResultsUrl(jobId, analysisType); + const redirectToAnalyticsExplorationPage = async () => { + const path = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + pageState: { + jobId, + analysisType, + }, + }); await navigateToPath(path); }; @@ -38,7 +45,7 @@ export const ViewResultsPanel: FC = ({ jobId, analysisType }) => { defaultMessage: 'View results for the analytics job.', } )} - onClick={redirectToAnalyticsManagementPage} + onClick={redirectToAnalyticsExplorationPage} data-test-subj="analyticsWizardViewResultsCard" /> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index c4c7a8a4ca11a..6d73340cc396a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -5,17 +5,13 @@ */ import React, { FC, useCallback, useState, useEffect } from 'react'; - import { i18n } from '@kbn/i18n'; - import { - Direction, - EuiButton, EuiCallOut, - EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, - EuiInMemoryTable, + EuiBasicTable, + EuiSearchBar, EuiSearchBarProps, EuiSpacer, } from '@elastic/eui'; @@ -43,6 +39,39 @@ import { getGroupQueryText, } from '../../../../../jobs/jobs_list/components/utils'; import { SourceSelection } from '../source_selection'; +import { filterAnalytics, AnalyticsSearchBar } from '../analytics_search_bar'; +import { AnalyticsEmptyPrompt } from './empty_prompt'; +import { useTableSettings } from './use_table_settings'; +import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button'; + +const filters: EuiSearchBarProps['filters'] = [ + { + type: 'field_value_selection', + field: 'job_type', + name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', { + defaultMessage: 'Type', + }), + multiSelect: 'or', + options: Object.values(ANALYSIS_CONFIG_TYPE).map((val) => ({ + value: val, + name: val, + view: getJobTypeBadge(val), + })), + }, + { + type: 'field_value_selection', + field: 'state', + name: i18n.translate('xpack.ml.dataframe.analyticsList.statusFilter', { + defaultMessage: 'Status', + }), + multiSelect: 'or', + options: Object.values(DATA_FRAME_TASK_STATE).map((val) => ({ + value: val, + name: val, + view: getTaskStateBadge(val), + })), + }, +]; function getItemIdToExpandedRowMap( itemIds: DataFrameAnalyticsId[], @@ -70,23 +99,23 @@ export const DataFrameAnalyticsList: FC = ({ const [isInitialized, setIsInitialized] = useState(false); const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false); const [isLoading, setIsLoading] = useState(false); - + const [filteredAnalytics, setFilteredAnalytics] = useState<{ + active: boolean; + items: DataFrameAnalyticsListRow[]; + }>({ + active: false, + items: [], + }); const [searchQueryText, setSearchQueryText] = useState(''); - const [analytics, setAnalytics] = useState([]); const [analyticsStats, setAnalyticsStats] = useState( undefined ); const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); - const [errorMessage, setErrorMessage] = useState(undefined); - const [searchError, setSearchError] = useState(undefined); - - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(10); - - const [sortField, setSortField] = useState(DataFrameAnalyticsListColumn.id); - const [sortDirection, setSortDirection] = useState('asc'); + // Query text/job_id based on url but only after getAnalytics is done first + // selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly + const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false); const disabled = !checkPermission('canCreateDataFrameAnalytics') || @@ -100,9 +129,29 @@ export const DataFrameAnalyticsList: FC = ({ blockRefresh ); - // Query text/job_id based on url but only after getAnalytics is done first - // selectedJobIdFromUrlInitialized makes sure the query is only run once since analytics is being refreshed constantly - const [selectedIdFromUrlInitialized, setSelectedIdFromUrlInitialized] = useState(false); + const setQueryClauses = (queryClauses: any) => { + if (queryClauses.length) { + const filtered = filterAnalytics(analytics, queryClauses); + setFilteredAnalytics({ active: true, items: filtered }); + } else { + setFilteredAnalytics({ active: false, items: [] }); + } + }; + + const filterList = () => { + if (searchQueryText !== '' && selectedIdFromUrlInitialized === true) { + // trigger table filtering with query for job id to trigger table filter + const query = EuiSearchBar.Query.parse(searchQueryText); + let clauses: any = []; + if (query && query.ast !== undefined && query.ast.clauses !== undefined) { + clauses = query.ast.clauses; + } + setQueryClauses(clauses); + } else { + setQueryClauses([]); + } + }; + useEffect(() => { if (selectedIdFromUrlInitialized === false && analytics.length > 0) { const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href); @@ -116,9 +165,15 @@ export const DataFrameAnalyticsList: FC = ({ setSelectedIdFromUrlInitialized(true); setSearchQueryText(queryText); + } else { + filterList(); } }, [selectedIdFromUrlInitialized, analytics]); + useEffect(() => { + filterList(); + }, [selectedIdFromUrlInitialized, searchQueryText]); + const getAnalyticsCallback = useCallback(() => getAnalytics(true), []); // Subscribe to the refresh observable to trigger reloading the analytics list. @@ -137,6 +192,10 @@ export const DataFrameAnalyticsList: FC = ({ isMlEnabledInSpace ); + const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings( + filteredAnalytics.active ? filteredAnalytics.items : analytics + ); + // Before the analytics have been loaded for the first time, display the loading indicator only. // Otherwise a user would see 'No data frame analytics found' during the initial loading. if (!isInitialized) { @@ -159,132 +218,45 @@ export const DataFrameAnalyticsList: FC = ({ if (analytics.length === 0) { return ( - <> - - {i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptTitle', { - defaultMessage: 'Create your first data frame analytics job', - })} - - } - actions={ - !isManagementTable - ? [ - setIsSourceIndexModalVisible(true)} - isDisabled={disabled} - color="primary" - iconType="plusInCircle" - fill - data-test-subj="mlAnalyticsCreateFirstButton" - > - {i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', { - defaultMessage: 'Create job', - })} - , - ] - : [] - } - data-test-subj="mlNoDataFrameAnalyticsFound" +
    + setIsSourceIndexModalVisible(true)} /> {isSourceIndexModalVisible === true && ( setIsSourceIndexModalVisible(false)} /> )} - +
    ); } - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - const itemIdToExpandedRowMap = getItemIdToExpandedRowMap(expandedRowItemIds, analytics); - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: analytics.length, - pageSizeOptions: [10, 20, 50], - hidePerPageOptions: false, - }; - - const handleSearchOnChange: EuiSearchBarProps['onChange'] = (search) => { - if (search.error !== null) { - setSearchError(search.error.message); - return false; - } - - setSearchError(undefined); - setSearchQueryText(search.queryText); - return true; - }; - - const search: EuiSearchBarProps = { - query: searchQueryText, - onChange: handleSearchOnChange, - box: { - incremental: true, - }, - filters: [ - { - type: 'field_value_selection', - field: 'job_type', - name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', { - defaultMessage: 'Type', - }), - multiSelect: 'or', - options: Object.values(ANALYSIS_CONFIG_TYPE).map((val) => ({ - value: val, - name: val, - view: getJobTypeBadge(val), - })), - }, - { - type: 'field_value_selection', - field: 'state', - name: i18n.translate('xpack.ml.dataframe.analyticsList.statusFilter', { - defaultMessage: 'Status', - }), - multiSelect: 'or', - options: Object.values(DATA_FRAME_TASK_STATE).map((val) => ({ - value: val, - name: val, - view: getTaskStateBadge(val), - })), - }, - ], - }; - - const onTableChange: EuiInMemoryTable['onTableChange'] = ({ - page = { index: 0, size: 10 }, - sort = { field: DataFrameAnalyticsListColumn.id, direction: 'asc' }, - }) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); + const stats = analyticsStats && ( + + + + ); - const { field, direction } = sort; - setSortField(field); - setSortDirection(direction); - }; + const managementStats = ( + + + {stats} + + + + + + ); return ( - <> +
    {modals} - + {!isManagementTable && } - - {analyticsStats && ( - - - - )} - + {!isManagementTable && stats} + {isManagementTable && managementStats} {!isManagementTable && ( @@ -300,22 +272,25 @@ export const DataFrameAnalyticsList: FC = ({
    - + + className="mlAnalyticsTable" columns={columns} - error={searchError} hasActions={false} isExpandable={true} isSelectable={false} - items={analytics} + items={pageOfItems} itemId={DataFrameAnalyticsListColumn.id} itemIdToExpandedRowMap={itemIdToExpandedRowMap} loading={isLoading} - onTableChange={onTableChange} - pagination={pagination} + onChange={onTableChange} + pagination={pagination!} sorting={sorting} - search={search} data-test-subj={isLoading ? 'mlAnalyticsTable loading' : 'mlAnalyticsTable loaded'} rowProps={(item) => ({ 'data-test-subj': `mlAnalyticsTableRow row-${item.id}`, @@ -326,6 +301,6 @@ export const DataFrameAnalyticsList: FC = ({ {isSourceIndexModalVisible === true && ( setIsSourceIndexModalVisible(false)} /> )} - +
    ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index 774864ae964a8..994357412510d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -26,6 +26,7 @@ export type Clause = Parameters[0]; type ExtractClauseType = T extends (x: any) => x is infer Type ? Type : never; export type TermClause = ExtractClauseType; export type FieldClause = ExtractClauseType; +export type Value = Parameters[0]; interface ProgressSection { phase: string; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/empty_prompt.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/empty_prompt.tsx new file mode 100644 index 0000000000000..fb173697b4572 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/empty_prompt.tsx @@ -0,0 +1,51 @@ +/* + * 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 React, { FC } from 'react'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface Props { + disabled: boolean; + isManagementTable: boolean; + onCreateFirstJobClick: () => void; +} + +export const AnalyticsEmptyPrompt: FC = ({ + disabled, + isManagementTable, + onCreateFirstJobClick, +}) => ( + + {i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptTitle', { + defaultMessage: 'Create your first data frame analytics job', + })} + + } + actions={ + !isManagementTable + ? [ + + {i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', { + defaultMessage: 'Create job', + })} + , + ] + : [] + } + data-test-subj="mlNoDataFrameAnalyticsFound" + /> +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index 645c6c276a6f9..95204f9ba09fc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; import { DataFrameAnalyticsListRow } from './common'; -import { ExpandedRowDetailsPane, SectionConfig } from './expanded_row_details_pane'; +import { ExpandedRowDetailsPane, SectionConfig, SectionItem } from './expanded_row_details_pane'; import { ExpandedRowJsonPane } from './expanded_row_json_pane'; import { ProgressBar } from './progress_bar'; import { @@ -28,6 +28,7 @@ import { getTaskStateBadge } from './use_columns'; import { getDataFrameAnalyticsProgressPhase, isCompletedAnalyticsJob } from './common'; import { isRegressionAnalysis, + getAnalysisType, ANALYSIS_CONFIG_TYPE, REGRESSION_STATS, isRegressionEvaluateResponse, @@ -76,6 +77,7 @@ export const ExpandedRow: FC = ({ item }) => { const resultsField = item.config.dest.results_field; const jobIsCompleted = isCompletedAnalyticsJob(item.stats); const isRegressionJob = isRegressionAnalysis(item.config.analysis); + const analysisType = getAnalysisType(item.config.analysis); const loadData = async () => { setIsLoadingGeneralization(true); @@ -160,25 +162,34 @@ export const ExpandedRow: FC = ({ item }) => { const stateValues: any = { ...item.stats }; + const analysisStatsValues = stateValues.analysis_stats + ? stateValues.analysis_stats[`${analysisType}_stats`] + : undefined; + if (item.config?.description) { stateValues.description = item.config.description; } delete stateValues.progress; - const state: SectionConfig = { - title: i18n.translate('xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state', { - defaultMessage: 'State', - }), - items: Object.entries(stateValues).map(([stateKey, stateValue]) => { + const stateItems = Object.entries(stateValues) + .map(([stateKey, stateValue]) => { const title = stateKey.toString(); if (title === 'state') { return { title, description: getTaskStateBadge(getItemDescription(stateValue)), }; + } else if (title !== 'analysis_stats') { + return { title, description: getItemDescription(stateValue) }; } - return { title, description: getItemDescription(stateValue) }; + }) + .filter((stateItem) => stateItem !== undefined); + + const state: SectionConfig = { + title: i18n.translate('xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.state', { + defaultMessage: 'State', }), + items: stateItems as SectionItem[], position: 'left', }; @@ -204,7 +215,7 @@ export const ExpandedRow: FC = ({ item }) => { }; }), ], - position: 'left', + position: 'right', }; const stats: SectionConfig = { @@ -221,9 +232,39 @@ export const ExpandedRow: FC = ({ item }) => { { title: 'model_memory_limit', description: item.config.model_memory_limit }, { title: 'version', description: item.config.version }, ], - position: 'right', + position: 'left', }; + const analysisStats: SectionConfig | undefined = analysisStatsValues + ? { + title: i18n.translate( + 'xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettings.analysisStats', + { + defaultMessage: 'Analysis stats', + } + ), + items: [ + { + title: 'timestamp', + description: formatHumanReadableDateTimeSeconds( + moment(analysisStatsValues.timestamp).unix() * 1000 + ), + }, + { + title: 'timing_stats', + description: getItemDescription(analysisStatsValues.timing_stats), + }, + ...Object.entries( + analysisStatsValues.parameters || analysisStatsValues.hyperparameters || {} + ).map(([stateKey, stateValue]) => { + const title = stateKey.toString(); + return { title, description: getItemDescription(stateValue) }; + }), + ], + position: 'right', + } + : undefined; + if (jobIsCompleted && isRegressionJob) { stats.items.push( { @@ -309,13 +350,30 @@ export const ExpandedRow: FC = ({ item }) => { ); } + const detailsSections: SectionConfig[] = [state, progress]; + const statsSections: SectionConfig[] = [stats]; + + if (analysisStats !== undefined) { + statsSections.push(analysisStats); + } + const tabs = [ { id: 'ml-analytics-job-details', name: i18n.translate('xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettingsLabel', { defaultMessage: 'Job details', }), - content: , + content: , + }, + { + id: 'ml-analytics-job-stats', + name: i18n.translate( + 'xpack.ml.dataframe.analyticsList.analyticsDetails.tabs.analyticsStatsLabel', + { + defaultMessage: 'Job stats', + } + ), + content: , }, { id: 'ml-analytics-job-json', diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index 7001681b6917a..ef1d373a55a12 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -23,7 +23,6 @@ import { getJobIdUrl, TAB_IDS } from '../../../../../util/get_selected_ids_url'; import { getAnalysisType, DataFrameAnalyticsId } from '../../../../common'; import { - getDataFrameAnalyticsProgress, getDataFrameAnalyticsProgressPhase, isDataFrameAnalyticsFailed, isDataFrameAnalyticsRunning, @@ -76,7 +75,6 @@ export const progressColumn = { name: i18n.translate('xpack.ml.dataframe.analyticsList.progress', { defaultMessage: 'Progress', }), - sortable: (item: DataFrameAnalyticsListRow) => getDataFrameAnalyticsProgress(item.stats), truncateText: true, render(item: DataFrameAnalyticsListRow) { const { currentPhase, progress, totalPhases } = getDataFrameAnalyticsProgressPhase(item.stats); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts new file mode 100644 index 0000000000000..57eb9f6857053 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts @@ -0,0 +1,119 @@ +/* + * 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 { useState } from 'react'; +import { Direction, EuiBasicTableProps, EuiTableSortingType } from '@elastic/eui'; +import sortBy from 'lodash/sortBy'; +import get from 'lodash/get'; +import { DataFrameAnalyticsListColumn, DataFrameAnalyticsListRow } from './common'; + +const PAGE_SIZE = 10; +const PAGE_SIZE_OPTIONS = [10, 25, 50]; + +const jobPropertyMap = { + ID: 'id', + Status: 'state', + Type: 'job_type', +}; + +interface AnalyticsBasicTableSettings { + pageIndex: number; + pageSize: number; + totalItemCount: number; + hidePerPageOptions: boolean; + sortField: string; + sortDirection: Direction; +} + +interface UseTableSettingsReturnValue { + onTableChange: EuiBasicTableProps['onChange']; + pageOfItems: DataFrameAnalyticsListRow[]; + pagination: EuiBasicTableProps['pagination']; + sorting: EuiTableSortingType; +} + +export function useTableSettings(items: DataFrameAnalyticsListRow[]): UseTableSettingsReturnValue { + const [tableSettings, setTableSettings] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + totalItemCount: 0, + hidePerPageOptions: false, + sortField: DataFrameAnalyticsListColumn.id, + sortDirection: 'asc', + }); + + const getPageOfItems = ( + list: any[], + index: number, + size: number, + sortField: string, + sortDirection: Direction + ) => { + list = sortBy(list, (item) => + get(item, jobPropertyMap[sortField as keyof typeof jobPropertyMap] || sortField) + ); + list = sortDirection === 'asc' ? list : list.reverse(); + const listLength = list.length; + + let pageStart = index * size; + if (pageStart >= listLength && listLength !== 0) { + // if the page start is larger than the number of items due to + // filters being applied or items being deleted, calculate a new page start + pageStart = Math.floor((listLength - 1) / size) * size; + + setTableSettings({ ...tableSettings, pageIndex: pageStart / size }); + } + return { + pageOfItems: list.slice(pageStart, pageStart + size), + totalItemCount: listLength, + }; + }; + + const onTableChange = ({ + page = { index: 0, size: PAGE_SIZE }, + sort = { field: DataFrameAnalyticsListColumn.id, direction: 'asc' }, + }: { + page?: { index: number; size: number }; + sort?: { field: string; direction: Direction }; + }) => { + const { index, size } = page; + const { field, direction } = sort; + + setTableSettings({ + ...tableSettings, + pageIndex: index, + pageSize: size, + sortField: field, + sortDirection: direction, + }); + }; + + const { pageIndex, pageSize, sortField, sortDirection } = tableSettings; + + const { pageOfItems, totalItemCount } = getPageOfItems( + items, + pageIndex, + pageSize, + sortField, + sortDirection + ); + + const pagination = { + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }; + + const sorting = { + sort: { + field: sortField, + direction: sortDirection, + }, + }; + + return { onTableChange, pageOfItems, pagination, sorting }; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx new file mode 100644 index 0000000000000..44a6572a3766c --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx @@ -0,0 +1,157 @@ +/* + * 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 React, { Dispatch, SetStateAction, FC, Fragment, useState } from 'react'; +import { + EuiSearchBar, + EuiSearchBarProps, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { stringMatch } from '../../../../../util/string_utils'; +import { + TermClause, + FieldClause, + Value, + DataFrameAnalyticsListRow, +} from '../analytics_list/common'; + +export function filterAnalytics( + items: DataFrameAnalyticsListRow[], + clauses: Array +) { + if (clauses.length === 0) { + return items; + } + + // keep count of the number of matches we make as we're looping over the clauses + // we only want to return items which match all clauses, i.e. each search term is ANDed + const matches: Record = items.reduce((p: Record, c) => { + p[c.id] = { + job: c, + count: 0, + }; + return p; + }, {}); + + clauses.forEach((c) => { + // the search term could be negated with a minus, e.g. -bananas + const bool = c.match === 'must'; + let js = []; + + if (c.type === 'term') { + // filter term based clauses, e.g. bananas + // match on id, description and memory_status + // if the term has been negated, AND the matches + if (bool === true) { + js = items.filter( + (item) => + stringMatch(item.id, c.value) === bool || + stringMatch(item.config.description, c.value) === bool || + stringMatch(item.stats?.memory_usage?.status, c.value) === bool + ); + } else { + js = items.filter( + (item) => + stringMatch(item.id, c.value) === bool && + stringMatch(item.config.description, c.value) === bool && + stringMatch(item.stats?.memory_usage?.status, c.value) === bool + ); + } + } else { + // filter other clauses, i.e. the filters for type and status + if (Array.isArray(c.value)) { + // job type value and status value are an array of string(s) e.g. c.value => ['failed', 'stopped'] + js = items.filter((item) => + (c.value as Value[]).includes( + item[c.field as keyof Pick] + ) + ); + } else { + js = items.filter( + (item) => item[c.field as keyof Pick] === c.value + ); + } + } + + js.forEach((j) => matches[j.id].count++); + }); + + // loop through the matches and return only those items which have match all the clauses + const filtered = Object.values(matches) + .filter((m) => (m && m.count) >= clauses.length) + .map((m) => m.job); + + return filtered; +} + +function getError(errorMessage: string | null) { + if (errorMessage) { + return i18n.translate('xpack.ml.analyticList.searchBar.invalidSearchErrorMessage', { + defaultMessage: 'Invalid search: {errorMessage}', + values: { errorMessage }, + }); + } + + return ''; +} + +interface Props { + filters: EuiSearchBarProps['filters']; + searchQueryText: string; + setSearchQueryText: Dispatch>; +} + +export const AnalyticsSearchBar: FC = ({ filters, searchQueryText, setSearchQueryText }) => { + const [errorMessage, setErrorMessage] = useState(null); + + const onChange: EuiSearchBarProps['onChange'] = ({ query, error }) => { + if (error) { + setErrorMessage(error.message); + } else if (query !== null && query.text !== undefined) { + setSearchQueryText(query.text); + setErrorMessage(null); + } + }; + + return ( + + + {searchQueryText === undefined && ( + + )} + {searchQueryText !== undefined && ( + + )} + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts new file mode 100644 index 0000000000000..3b901f5063eb1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { AnalyticsSearchBar, filterAnalytics } from './analytics_search_bar'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/errors.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/errors.tsx index f723ad1a752bf..bcbba67e9cc60 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/errors.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_errors/errors.tsx @@ -93,11 +93,11 @@ function title(statuses: Statuses) { } } -function ImportError(error: any, key: number) { +const ImportError: FC<{ error: any }> = ({ error }) => { const errorObj = toString(error); return ( - -

    {errorObj.msg}

    + <> +

    {errorObj.msg}

    {errorObj.more !== undefined && ( )} -
    + ); -} +}; function toString(error: any): ImportError { if (typeof error === 'string') { @@ -127,11 +127,11 @@ function toString(error: any): ImportError { return { msg: error.msg }; } else if (error.error !== undefined) { if (typeof error.error === 'object') { - if (error.error.msg !== undefined) { + if (error.error.reason !== undefined) { // this will catch a bulk ingest failure - const errorObj: ImportError = { msg: error.error.msg }; - if (error.error.body !== undefined) { - errorObj.more = error.error.response; + const errorObj: ImportError = { msg: error.error.reason }; + if (error.error.root_cause !== undefined) { + errorObj.more = JSON.stringify(error.error.root_cause); } return errorObj; } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx index 16d9e0c5418fa..1f2c97b128e3f 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx @@ -13,6 +13,7 @@ import { EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; import { DataRecognizer } from '../../../../components/data_recognizer'; +import { getBasePath } from '../../../../util/dependency_cache'; interface Props { indexPattern: IndexPattern; @@ -20,6 +21,7 @@ interface Props { export const ActionsPanel: FC = ({ indexPattern }) => { const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); + const basePath = getBasePath(); const recognizerResults = { count: 0, @@ -31,7 +33,7 @@ export const ActionsPanel: FC = ({ indexPattern }) => { function openAdvancedJobWizard() { // TODO - pass the search string to the advanced job page as well as the index pattern // (add in with new advanced job wizard?) - window.open(`#/jobs/new_job/advanced?index=${indexPattern}`, '_self'); + window.open(`${basePath.get()}/app/ml/jobs/new_job/advanced?index=${indexPattern.id}`, '_self'); } // Note we use display:none for the DataRecognizer section as it needs to be @@ -86,7 +88,7 @@ export const ActionsPanel: FC = ({ indexPattern }) => { 'Use the full range of options to create a job for more advanced use cases', })} onClick={openAdvancedJobWizard} - href={`#/jobs/new_job/advanced?index=${indexPattern}`} + href={`${basePath.get()}/app/ml/jobs/new_job/advanced?index=${indexPattern.id}`} data-test-subj="mlDataVisualizerCreateAdvancedJobCard" />
    diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts index 4d697bcda1a06..e0ed2ea6cf5e0 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts @@ -21,6 +21,7 @@ import { ExplorerChartsData } from './explorer_charts/explorer_charts_container_ import { EXPLORER_ACTION } from './explorer_constants'; import { AppStateSelectedCells, TimeRangeBounds } from './explorer_utils'; import { explorerReducer, getExplorerDefaultState, ExplorerState } from './reducers'; +import { ExplorerAppState } from '../../../common/types/ml_url_generator'; export const ALLOW_CELL_RANGE_SELECTION = true; @@ -49,24 +50,6 @@ const explorerState$: Observable = explorerFilteredAction$.pipe( shareReplay(1) ); -export interface ExplorerAppState { - mlExplorerSwimlane: { - selectedType?: string; - selectedLanes?: string[]; - selectedTimes?: number[]; - showTopFieldValues?: boolean; - viewByFieldName?: string; - viewByPerPage?: number; - viewByFromPage?: number; - }; - mlExplorerFilter: { - influencersFilterQuery?: unknown; - filterActive?: boolean; - filteredFields?: string[]; - queryString?: string; - }; -} - const explorerAppState$: Observable = explorerState$.pipe( map( (state: ExplorerState): ExplorerAppState => { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js index 74072aa7e9638..6b8d1d80aeda5 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js @@ -55,7 +55,7 @@ export function ResultLinks({ jobs }) { aria-label={openJobsInSingleMetricViewerText} className="results-button" isDisabled={singleMetricEnabled === false || jobActionsDisabled === true} - data-test-subj={`openJobsInSingleMetricViewer openJobsInSingleMetricViewer-${jobs[0].id}`} + data-test-subj="mlOpenJobsInSingleMetricViewerButton" /> )} @@ -66,7 +66,7 @@ export function ResultLinks({ jobs }) { aria-label={openJobsInAnomalyExplorerText} className="results-button" isDisabled={jobActionsDisabled === true} - data-test-subj={`openJobsInAnomalyExplorer openJobsInSingleAnomalyExplorer-${jobs[0].id}`} + data-test-subj="mlOpenJobsInAnomalyExplorerButton" />
    diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index f90bbf3cf3fe6..fa4ea09b89ff9 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { sortBy } from 'lodash'; +import sortBy from 'lodash/sortBy'; import moment from 'moment'; import { toLocaleString } from '../../../../util/string_utils'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js index a011f21fddfef..44460c0fb8139 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js @@ -57,6 +57,7 @@ class MultiJobActionsMenuUI extends Component { disabled={ anyJobsDeleting || (this.canDeleteJob === false && this.canStartStopDatafeed === false) } + data-test-subj="mlADJobListMultiSelectManagementActionsButton" /> ); @@ -69,6 +70,7 @@ class MultiJobActionsMenuUI extends Component { this.props.showDeleteJobModal(this.props.jobs); this.closePopover(); }} + data-test-subj="mlADJobListMultiSelectDeleteJobActionButton" > this.togglePopover()} disabled={this.canUpdateJob === false} + data-test-subj="mlADJobListMultiSelectEditJobGroupsButton" /> ); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js index 82563b083b8cc..b9ea18b5d2ed8 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js @@ -24,7 +24,10 @@ export class MultiJobActions extends Component { render() { const jobsSelected = this.props.selectedJobs.length > 0; return ( -
    +
    {jobsSelected && ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts index 328cd1a5ef8d7..d61e75fd21b5a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/chart_loader/chart_loader.ts @@ -71,9 +71,7 @@ export class ChartLoader { splitFieldName, splitFieldValue ); - if (resp.error !== undefined) { - throw resp.error; - } + return resp.results; } return {}; @@ -105,9 +103,7 @@ export class ChartLoader { aggFieldPairNames, splitFieldName ); - if (resp.error !== undefined) { - throw resp.error; - } + return resp.results; } return {}; diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx index 1a30b1637fb8d..6a9c66eec83bc 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx @@ -23,7 +23,7 @@ import { export const AccessDeniedPage = () => ( - + diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx index c4508a8c19c5b..395a570083c0d 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx @@ -48,7 +48,7 @@ export const ViewLink: FC = ({ item }) => { iconType="visTable" aria-label={viewJobResultsButtonText} className="results-button" - data-test-subj="mlAnalyticsJobViewButton" + data-test-subj="mlOverviewAnalyticsJobViewButton" isDisabled={disabled} > {i18n.translate('xpack.ml.overview.analytics.viewActionName', { diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx index 65e7ba9e8ab52..be8038cc5049d 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx @@ -100,6 +100,7 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { fill iconType="plusInCircle" isDisabled={jobCreationDisabled} + data-test-subj="mlOverviewCreateDFAJobButton" > {i18n.translate('xpack.ml.overview.analyticsList.createJobButtonText', { defaultMessage: 'Create job', diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index 03b66f5c369c1..0bfd2c2e49232 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -184,6 +184,7 @@ export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => { fill iconType="plusInCircle" isDisabled={jobCreationDisabled} + data-test-subj="mlOverviewCreateADJobButton" > {i18n.translate('xpack.ml.overview.anomalyDetection.createJobButtonText', { defaultMessage: 'Create job', diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index 56c9a19723fba..22a17c4ea089a 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -75,7 +75,6 @@ const MlRoutes: FC<{ pageDeps: PageDependencies; }> = ({ pageDeps }) => { const navigateToPath = useNavigateToPath(); - return ( <> {Object.entries(routes).map(([name, routeFactory]) => { diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 62d7e82a214b5..2f2fc77283ef7 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -22,7 +22,8 @@ import { useSelectedCells } from '../../explorer/hooks/use_selected_cells'; import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; import { useExplorerData } from '../../explorer/actions'; -import { ExplorerAppState, explorerService } from '../../explorer/explorer_dashboard_service'; +import { ExplorerAppState } from '../../../../common/types/ml_url_generator'; +import { explorerService } from '../../explorer/explorer_dashboard_service'; import { getDateFormatTz } from '../../explorer/explorer_utils'; import { useJobSelection } from '../../components/job_selector/use_job_selection'; import { useShowCharts } from '../../components/controls/checkbox_showcharts'; diff --git a/x-pack/plugins/ml/public/application/services/field_format_service.ts b/x-pack/plugins/ml/public/application/services/field_format_service.ts index 1a5d22e7320a5..b66dac15b1364 100644 --- a/x-pack/plugins/ml/public/application/services/field_format_service.ts +++ b/x-pack/plugins/ml/public/application/services/field_format_service.ts @@ -77,7 +77,7 @@ class FieldFormatService { const fieldList = fullIndexPattern.fields; const field = fieldList.getByName(fieldName); if (field !== undefined) { - fieldFormat = field.format; + fieldFormat = fullIndexPattern.getFormatterForField(field); } } @@ -104,7 +104,9 @@ class FieldFormatService { if (dtr.field_name !== undefined && esAgg !== 'cardinality') { const field = fieldList.getByName(dtr.field_name); if (field !== undefined) { - formatsByDetector[dtr.detector_index!] = field.format; + formatsByDetector[dtr.detector_index!] = indexPatternData.getFormatterForField( + field + ); } } }); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap index 21f505cff9aec..34ee304b8bd41 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap @@ -7,6 +7,7 @@ exports[`NewCalendar Renders new calendar form 1`] = ` /> {isGlobalCalendar === false && ( @@ -166,6 +167,7 @@ export const CalendarForm = ({ selectedOptions={selectedJobOptions} onChange={onJobSelection} isDisabled={saving === true || canCreateCalendar === false} + data-test-subj="mlCalendarJobSelection" /> @@ -183,6 +185,7 @@ export const CalendarForm = ({ selectedOptions={selectedGroupOptions} onChange={onGroupSelection} isDisabled={saving === true || canCreateCalendar === false} + data-test-subj="mlCalendarJobGroupSelection" /> diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap index df22f1a08ff42..e3edc563e7c3e 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap @@ -34,6 +34,7 @@ exports[`EventsTable Renders events table with no search bar 1`] = ` }, ] } + data-test-subj="mlCalendarEventsTable" itemId="event_id" items={ Array [ @@ -56,6 +57,7 @@ exports[`EventsTable Renders events table with no search bar 1`] = ` } } responsive={true} + rowProps={[Function]} sorting={ Object { "sort": Object { @@ -103,6 +105,7 @@ exports[`EventsTable Renders events table with search bar 1`] = ` }, ] } + data-test-subj="mlCalendarEventsTable" itemId="event_id" items={ Array [ @@ -125,6 +128,7 @@ exports[`EventsTable Renders events table with search bar 1`] = ` } } responsive={true} + rowProps={[Function]} search={ Object { "box": Object { @@ -133,7 +137,7 @@ exports[`EventsTable Renders events table with search bar 1`] = ` "filters": Array [], "toolsRight": Array [ , ( { onDeleteClick(event.event_id); @@ -105,7 +106,7 @@ export const EventsTable = ({ ({ + 'data-test-subj': `mlCalendarEventListRow row-${item.description}`, + })} /> ); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js index 0c8e1f07eb355..2faac7d850fa9 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js @@ -51,7 +51,7 @@ describe('ImportModal', () => { instance.setState(testState); wrapper.update(); expect(wrapper.state('selectedEvents').length).toBe(2); - const deleteButton = wrapper.find('[data-test-subj="mlEventDelete"]'); + const deleteButton = wrapper.find('[data-test-subj="mlCalendarEventDeleteButton"]'); const button = deleteButton.find('EuiButtonEmpty').first(); button.simulate('click'); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index 7efc37d4bf8ce..1fe16e4588bd7 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -339,7 +339,7 @@ class NewCalendarUI extends Component { return ( - + { test('Import modal shown on Import Events button click', () => { const wrapper = mountWithIntl(); - const importButton = wrapper.find('[data-test-subj="mlImportEvents"]'); + const importButton = wrapper.find('[data-test-subj="mlCalendarImportEventsButton"]'); const button = importButton.find('EuiButton'); button.simulate('click'); @@ -127,7 +127,7 @@ describe('NewCalendar', () => { test('New event modal shown on New event button click', () => { const wrapper = mountWithIntl(); - const importButton = wrapper.find('[data-test-subj="mlNewEvent"]'); + const importButton = wrapper.find('[data-test-subj="mlCalendarNewEventButton"]'); const button = importButton.find('EuiButton'); button.simulate('click'); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap index aeeeeef63a71e..0f7585e3a9fa6 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap @@ -7,6 +7,7 @@ exports[`CalendarsList Renders calendar list with calendars 1`] = ` /> - + - + +
    , - +
    `; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js index b81cc6fbb4c30..77331c4a987dc 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -46,8 +46,14 @@ export const CalendarsListTable = ({ truncateText: true, scope: 'row', render: (id) => ( - {id} + + {id} + ), + 'data-test-subj': 'mlCalendarListColumnId', }, { field: 'job_ids_string', @@ -68,6 +74,7 @@ export const CalendarsListTable = ({ jobList ); }, + 'data-test-subj': 'mlCalendarListColumnJobs', }, { field: 'events_length', @@ -80,6 +87,7 @@ export const CalendarsListTable = ({ defaultMessage: '{eventsLength, plural, one {# event} other {# events}}', values: { eventsLength }, }), + 'data-test-subj': 'mlCalendarListColumnEvents', }, ]; @@ -106,6 +114,7 @@ export const CalendarsListTable = ({ isDisabled={ canDeleteCalendar === false || mlNodesAvailable === false || itemsSelected === false } + data-test-subj="mlCalendarButtonDelete" > +
    ({ + 'data-test-subj': `mlCalendarListRow row-${item.calendar_id}`, + })} /> - +
    ); }; diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap index 0ce19b8fa3d57..6e9cd17deabee 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap @@ -7,6 +7,7 @@ exports[`AddItemPopover calls addItems with multiple items on clicking Add butto button={ ); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap index 447d79ffff32a..c2fab64473228 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap @@ -7,6 +7,7 @@ exports[`EditFilterList adds new items to filter list 1`] = ` /> , , - + - + , - , + , - ], + ] + } + />, + ], + } } - } - selection={ - Object { - "onSelectionChange": [Function], - "selectable": [Function], - "selectableMessage": [Function], + selection={ + Object { + "onSelectionChange": [Function], + "selectable": [Function], + "selectableMessage": [Function], + } } - } - sorting={ - Object { - "sort": Object { - "direction": "asc", - "field": "filter_id", - }, + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "filter_id", + }, + } } - } - tableLayout="fixed" - /> + tableLayout="fixed" + /> +
    `; exports[`Filter Lists Table renders with filter lists supplied 1`] = ` - + , - , - ], + "box": Object { + "incremental": true, + }, + "filters": Array [], + "toolsRight": Array [ + , + , + ], + } } - } - selection={ - Object { - "onSelectionChange": [Function], - "selectable": [Function], - "selectableMessage": [Function], + selection={ + Object { + "onSelectionChange": [Function], + "selectable": [Function], + "selectableMessage": [Function], + } } - } - sorting={ - Object { - "sort": Object { - "direction": "asc", - "field": "filter_id", - }, + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "filter_id", + }, + } } - } - tableLayout="fixed" - /> + tableLayout="fixed" + /> +
    `; diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js index 270d5fa350cae..75c72fdab7307 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js @@ -89,7 +89,7 @@ export class FilterListsUI extends Component { return ( - + - refreshFilterLists()}> + refreshFilterLists()} + data-test-subj="mlFilterListRefreshButton" + > ); } else { @@ -47,6 +48,7 @@ function UsedByIcon({ usedBy }) { aria-label={i18n.translate('xpack.ml.settings.filterLists.table.notInUseAriaLabel', { defaultMessage: 'Not in use', })} + data-test-subj="mlFilterListUsedByIcon notInUse" /> ); } @@ -82,10 +84,16 @@ function getColumns() { defaultMessage: 'ID', }), render: (id) => ( - {id} + + {id} + ), sortable: true, scope: 'row', + 'data-test-subj': 'mlFilterListColumnId', }, { field: 'description', @@ -93,6 +101,7 @@ function getColumns() { defaultMessage: 'Description', }), sortable: true, + 'data-test-subj': 'mlFilterListColumnDescription', }, { field: 'item_count', @@ -100,6 +109,7 @@ function getColumns() { defaultMessage: 'Item count', }), sortable: true, + 'data-test-subj': 'mlFilterListColumnItemCount', }, { field: 'used_by', @@ -108,6 +118,7 @@ function getColumns() { }), render: (usedBy) => , sortable: true, + 'data-test-subj': 'mlFilterListColumnInUse', }, ]; @@ -189,7 +200,7 @@ export function FilterListsTable({ ) : ( - +
    ({ + 'data-test-subj': `mlCalendarListRow row-${item.filter_id}`, + })} /> - +
    )}
    ); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx index 93bb62fa1fc58..9d2c49a95fec4 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -141,6 +141,7 @@ export class EntityControl extends Component ); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js index 6527aa3801da9..4fec8b01530f3 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js @@ -31,7 +31,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; export function Modal(props) { return ( - + ( + 'mlManagement', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + return url; +} + +export function createAnomalyDetectionCreateJobSelectType( + appBasePath: string, + pageState: MlGenericUrlState['pageState'] +): string { + return createIndexBasedMlUrl( + appBasePath, + ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, + pageState + ); +} + +/** + * Creates URL to the Anomaly Explorer page + */ +export function createExplorerUrl( + appBasePath: string, + params: ExplorerUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.ANOMALY_EXPLORER}`; + + if (!params) { + return url; + } + const { + refreshInterval, + timeRange, + jobIds, + query, + mlExplorerSwimlane = {}, + mlExplorerFilter = {}, + } = params; + const appState: Partial = { + mlExplorerSwimlane, + mlExplorerFilter, + }; + if (query) appState.query = query; + + if (jobIds) { + const queryState: Partial = { + ml: { + jobIds, + }, + }; + + if (timeRange) queryState.time = timeRange; + if (refreshInterval) queryState.refreshInterval = refreshInterval; + + url = setStateToKbnUrl>( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + url = setStateToKbnUrl>( + '_a', + appState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + return url; +} + +/** + * Creates URL to the SingleMetricViewer page + */ +export function createSingleMetricViewerUrl( + appBasePath: string, + params: TimeSeriesExplorerUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.SINGLE_METRIC_VIEWER}`; + if (!params) { + return url; + } + const { timeRange, jobIds, refreshInterval, zoom, query, detectorIndex, entities } = params; + + const queryState: TimeSeriesExplorerGlobalState = { + ml: { + jobIds, + }, + refreshInterval, + time: timeRange, + }; + + const appState: Partial = {}; + const mlTimeSeriesExplorer: Partial = {}; + + if (detectorIndex !== undefined) { + mlTimeSeriesExplorer.detectorIndex = detectorIndex; + } + if (entities !== undefined) { + mlTimeSeriesExplorer.entities = entities; + } + appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer; + + if (zoom) appState.zoom = zoom; + if (query) + appState.query = { + query_string: query, + }; + url = setStateToKbnUrl( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + url = setStateToKbnUrl( + '_a', + appState, + { useHash: false, storeInHashQuery: false }, + url + ); + + return url; +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/common.ts b/x-pack/plugins/ml/public/ml_url_generator/common.ts new file mode 100644 index 0000000000000..57cfc52045282 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/common.ts @@ -0,0 +1,55 @@ +/* + * 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 isEmpty from 'lodash/isEmpty'; +import { MlGenericUrlState } from '../../common/types/ml_url_generator'; +import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; + +export function extractParams(urlState: UrlState) { + // page should be guaranteed to exist here but is unknown + // @ts-ignore + const { page, ...params } = urlState; + return { page, params }; +} + +/** + * Creates generic index based search ML url + * e.g. `jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a` + */ +export function createIndexBasedMlUrl( + appBasePath: string, + page: MlGenericUrlState['page'], + pageState: MlGenericUrlState['pageState'] +): string { + const { globalState, appState, index, savedSearchId, ...restParams } = pageState; + let url = `${appBasePath}/${page}`; + + if (index !== undefined && savedSearchId === undefined) { + url = `${url}?index=${index}`; + } + if (index === undefined && savedSearchId !== undefined) { + url = `${url}?savedSearchId=${savedSearchId}`; + } + + if (!isEmpty(restParams)) { + Object.keys(restParams).forEach((key) => { + url = setStateToKbnUrl( + key, + restParams[key], + { useHash: false, storeInHashQuery: false }, + url + ); + }); + } + + if (globalState) { + url = setStateToKbnUrl('_g', globalState, { useHash: false, storeInHashQuery: false }, url); + } + if (appState) { + url = setStateToKbnUrl('_a', appState, { useHash: false, storeInHashQuery: false }, url); + } + return url; +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts new file mode 100644 index 0000000000000..8cf10a2acb64f --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -0,0 +1,70 @@ +/* + * 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. + */ + +/** + * Creates URL to the DataFrameAnalytics page + */ +import { + DataFrameAnalyticsExplorationQueryState, + DataFrameAnalyticsExplorationUrlState, + DataFrameAnalyticsQueryState, + DataFrameAnalyticsUrlState, +} from '../../common/types/ml_url_generator'; +import { ML_PAGES } from '../../common/constants/ml_url_generator'; +import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; + +export function createDataFrameAnalyticsJobManagementUrl( + appBasePath: string, + mlUrlGeneratorState: DataFrameAnalyticsUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE}`; + + if (mlUrlGeneratorState) { + const { jobId, groupIds } = mlUrlGeneratorState; + const queryState: Partial = { + jobId, + groupIds, + }; + + url = setStateToKbnUrl>( + 'mlManagement', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + return url; +} + +/** + * Creates URL to the DataFrameAnalytics Exploration page + */ +export function createDataFrameAnalyticsExplorationUrl( + appBasePath: string, + mlUrlGeneratorState: DataFrameAnalyticsExplorationUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION}`; + + if (mlUrlGeneratorState) { + const { jobId, analysisType } = mlUrlGeneratorState; + const queryState: DataFrameAnalyticsExplorationQueryState = { + ml: { + jobId, + analysisType, + }, + }; + + url = setStateToKbnUrl( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + return url; +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts new file mode 100644 index 0000000000000..24693df5025d9 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.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. + */ + +/** + * Creates URL to the Data Visualizer page + */ +import { DataVisualizerUrlState, MlGenericUrlState } from '../../common/types/ml_url_generator'; +import { createIndexBasedMlUrl } from './common'; +import { ML_PAGES } from '../../common/constants/ml_url_generator'; + +export function createDataVisualizerUrl( + appBasePath: string, + { page }: DataVisualizerUrlState +): string { + return `${appBasePath}/${page}`; +} + +/** + * Creates URL to the Index Data Visualizer + */ +export function createIndexDataVisualizerUrl( + appBasePath: string, + pageState: MlGenericUrlState['pageState'] +): string { + return createIndexBasedMlUrl(appBasePath, ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, pageState); +} diff --git a/x-pack/plugins/ml/public/ml_url_generator/index.ts b/x-pack/plugins/ml/public/ml_url_generator/index.ts new file mode 100644 index 0000000000000..1579b187278c4 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/index.ts @@ -0,0 +1,6 @@ +/* + * 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 { MlUrlGenerator, registerUrlGenerator } from './ml_url_generator'; diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts new file mode 100644 index 0000000000000..55bc6d3668de7 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts @@ -0,0 +1,262 @@ +/* + * 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 { MlUrlGenerator } from './ml_url_generator'; +import { ML_PAGES } from '../../common/constants/ml_url_generator'; +import { ANALYSIS_CONFIG_TYPE } from '../../common/types/ml_url_generator'; + +describe('MlUrlGenerator', () => { + const urlGenerator = new MlUrlGenerator({ + appBasePath: '/app/ml', + useHash: false, + }); + + describe('AnomalyDetection', () => { + describe('Job Management Page', () => { + it('should generate valid URL for the Anomaly Detection job management page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + }); + expect(url).toBe('/app/ml/jobs'); + }); + + it('should generate valid URL for the Anomaly Detection job management page for job', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState: { + jobId: 'fq_single_1', + }, + }); + expect(url).toBe('/app/ml/jobs?mlManagement=(jobId:fq_single_1)'); + }); + + it('should generate valid URL for the Anomaly Detection job management page for groupIds', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState: { + groupIds: ['farequote', 'categorization'], + }, + }); + expect(url).toBe('/app/ml/jobs?mlManagement=(groupIds:!(farequote,categorization))'); + }); + + it('should generate valid URL for the page for selecting the type of anomaly detection job to create', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, + pageState: { + index: `3da93760-e0af-11ea-9ad3-3bcfc330e42a`, + globalState: { + time: { + from: 'now-30m', + to: 'now', + }, + }, + }, + }); + expect(url).toBe( + '/app/ml/jobs/new_job/step/job_type?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_g=(time:(from:now-30m,to:now))' + ); + }); + }); + + describe('Anomaly Explorer Page', () => { + it('should generate valid URL for the Anomaly Explorer page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_EXPLORER, + pageState: { + jobIds: ['fq_single_1'], + mlExplorerSwimlane: { viewByFromPage: 2, viewByPerPage: 20 }, + refreshInterval: { + pause: false, + value: 0, + }, + timeRange: { + from: '2019-02-07T00:00:00.000Z', + to: '2020-08-13T17:15:00.000Z', + mode: 'absolute', + }, + query: { + analyze_wildcard: true, + query: '*', + }, + }, + }); + expect(url).toBe( + "/app/ml/explorer?_g=(ml:(jobIds:!(fq_single_1)),refreshInterval:(pause:!f,value:0),time:(from:'2019-02-07T00:00:00.000Z',mode:absolute,to:'2020-08-13T17:15:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20),query:(analyze_wildcard:!t,query:'*'))" + ); + }); + it('should generate valid URL for the Anomaly Explorer page for multiple jobIds', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_EXPLORER, + pageState: { + jobIds: ['fq_single_1', 'logs_categorization_1'], + timeRange: { + from: '2019-02-07T00:00:00.000Z', + to: '2020-08-13T17:15:00.000Z', + mode: 'absolute', + }, + }, + }); + expect(url).toBe( + "/app/ml/explorer?_g=(ml:(jobIds:!(fq_single_1,logs_categorization_1)),time:(from:'2019-02-07T00:00:00.000Z',mode:absolute,to:'2020-08-13T17:15:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:())" + ); + }); + }); + + describe('Single Metric Viewer Page', () => { + it('should generate valid URL for the Single Metric Viewer page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { + jobIds: ['logs_categorization_1'], + refreshInterval: { + pause: false, + value: 0, + }, + timeRange: { + from: '2020-07-12T00:39:02.912Z', + to: '2020-07-22T15:52:18.613Z', + mode: 'absolute', + }, + query: { + analyze_wildcard: true, + query: '*', + }, + }, + }); + expect(url).toBe( + "/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(logs_categorization_1)),refreshInterval:(pause:!f,value:0),time:(from:'2020-07-12T00:39:02.912Z',mode:absolute,to:'2020-07-22T15:52:18.613Z'))&_a=(mlTimeSeriesExplorer:(),query:(query_string:(analyze_wildcard:!t,query:'*')))" + ); + }); + + it('should generate valid URL for the Single Metric Viewer page with extra settings', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { + jobIds: ['logs_categorization_1'], + detectorIndex: 0, + entities: { mlcategory: '2' }, + refreshInterval: { + pause: false, + value: 0, + }, + timeRange: { + from: '2020-07-12T00:39:02.912Z', + to: '2020-07-22T15:52:18.613Z', + mode: 'absolute', + }, + zoom: { + from: '2020-07-20T23:58:29.367Z', + to: '2020-07-21T11:00:13.173Z', + }, + query: { + analyze_wildcard: true, + query: '*', + }, + }, + }); + expect(url).toBe( + "/app/ml/timeseriesexplorer?_g=(ml:(jobIds:!(logs_categorization_1)),refreshInterval:(pause:!f,value:0),time:(from:'2020-07-12T00:39:02.912Z',mode:absolute,to:'2020-07-22T15:52:18.613Z'))&_a=(mlTimeSeriesExplorer:(detectorIndex:0,entities:(mlcategory:'2')),query:(query_string:(analyze_wildcard:!t,query:'*')),zoom:(from:'2020-07-20T23:58:29.367Z',to:'2020-07-21T11:00:13.173Z'))" + ); + }); + }); + }); + + describe('DataFrameAnalytics', () => { + describe('JobManagement Page', () => { + it('should generate valid URL for the Data Frame Analytics job management page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + }); + expect(url).toBe('/app/ml/data_frame_analytics'); + }); + it('should generate valid URL for the Data Frame Analytics job management page with jobId', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + pageState: { + jobId: 'grid_regression_1', + }, + }); + expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(jobId:grid_regression_1)'); + }); + + it('should generate valid URL for the Data Frame Analytics job management page with groupIds', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + pageState: { + groupIds: ['group_1', 'group_2'], + }, + }); + expect(url).toBe('/app/ml/data_frame_analytics?mlManagement=(groupIds:!(group_1,group_2))'); + }); + }); + + describe('ExplorationPage', () => { + it('should generate valid URL for the Data Frame Analytics exploration page for job', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + pageState: { + jobId: 'grid_regression_1', + analysisType: ANALYSIS_CONFIG_TYPE.REGRESSION, + }, + }); + expect(url).toBe( + '/app/ml/data_frame_analytics/exploration?_g=(ml:(analysisType:regression,jobId:grid_regression_1))' + ); + }); + }); + }); + + describe('DataVisualizer', () => { + it('should generate valid URL for the Data Visualizer page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER, + }); + expect(url).toBe('/app/ml/datavisualizer'); + }); + + it('should generate valid URL for the File Data Visualizer import page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_FILE, + }); + expect(url).toBe('/app/ml/filedatavisualizer'); + }); + + it('should generate valid URL for the Index Data Visualizer select index pattern or saved search page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_INDEX_SELECT, + }); + expect(url).toBe('/app/ml/datavisualizer_index_select'); + }); + + it('should generate valid URL for the Index Data Visualizer Viewer page', async () => { + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, + pageState: { + index: '3da93760-e0af-11ea-9ad3-3bcfc330e42a', + globalState: { + time: { + from: 'now-30m', + to: 'now', + }, + }, + }, + }); + expect(url).toBe( + '/app/ml/jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_g=(time:(from:now-30m,to:now))' + ); + }); + }); + + it('should throw an error in case the page is not provided', async () => { + expect.assertions(1); + + // @ts-ignore + await urlGenerator.createUrl({ jobIds: ['test-job'] }).catch((e) => { + expect(e.message).toEqual('Page type is not provided or unknown'); + }); + }); +}); diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts new file mode 100644 index 0000000000000..b69260d8d4157 --- /dev/null +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts @@ -0,0 +1,91 @@ +/* + * 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 } from 'kibana/public'; +import { + SharePluginSetup, + UrlGeneratorsDefinition, + UrlGeneratorState, +} from '../../../../../src/plugins/share/public'; +import { MlStartDependencies } from '../plugin'; +import { ML_PAGES, ML_APP_URL_GENERATOR } from '../../common/constants/ml_url_generator'; +import { MlUrlGeneratorState } from '../../common/types/ml_url_generator'; +import { + createAnomalyDetectionJobManagementUrl, + createAnomalyDetectionCreateJobSelectType, + createExplorerUrl, + createSingleMetricViewerUrl, +} from './anomaly_detection_urls_generator'; +import { + createDataFrameAnalyticsJobManagementUrl, + createDataFrameAnalyticsExplorationUrl, +} from './data_frame_analytics_urls_generator'; +import { + createIndexDataVisualizerUrl, + createDataVisualizerUrl, +} from './data_visualizer_urls_generator'; + +declare module '../../../../../src/plugins/share/public' { + export interface UrlGeneratorStateMapping { + [ML_APP_URL_GENERATOR]: UrlGeneratorState; + } +} + +interface Params { + appBasePath: string; + useHash: boolean; +} + +export class MlUrlGenerator implements UrlGeneratorsDefinition { + constructor(private readonly params: Params) {} + + public readonly id = ML_APP_URL_GENERATOR; + + public readonly createUrl = async (mlUrlGeneratorState: MlUrlGeneratorState): Promise => { + const appBasePath = this.params.appBasePath; + switch (mlUrlGeneratorState.page) { + case ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE: + return createAnomalyDetectionJobManagementUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.ANOMALY_EXPLORER: + return createExplorerUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: + return createAnomalyDetectionCreateJobSelectType( + appBasePath, + mlUrlGeneratorState.pageState + ); + case ML_PAGES.SINGLE_METRIC_VIEWER: + return createSingleMetricViewerUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE: + return createDataFrameAnalyticsJobManagementUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION: + return createDataFrameAnalyticsExplorationUrl(appBasePath, mlUrlGeneratorState.pageState); + case ML_PAGES.DATA_VISUALIZER: + case ML_PAGES.DATA_VISUALIZER_FILE: + case ML_PAGES.DATA_VISUALIZER_INDEX_SELECT: + return createDataVisualizerUrl(appBasePath, mlUrlGeneratorState); + case ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER: + return createIndexDataVisualizerUrl(appBasePath, mlUrlGeneratorState.pageState); + default: + throw new Error('Page type is not provided or unknown'); + } + }; +} + +/** + * Registers the URL generator + */ +export function registerUrlGenerator( + share: SharePluginSetup, + core: CoreSetup +) { + const baseUrl = core.http.basePath.prepend('/app/ml'); + share.urlGenerators.registerUrlGenerator( + new MlUrlGenerator({ + appBasePath: baseUrl, + useHash: core.uiSettings.get('state:storeInSessionStorage'), + }) + ); +} diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 214b393a0fda9..3e8ab99e341ad 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -34,7 +34,7 @@ import { registerFeature } from './register_feature'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { registerMlUiActions } from './ui_actions'; import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; -import { registerUrlGenerator } from './url_generator'; +import { registerUrlGenerator } from './ml_url_generator'; import { isFullLicense, isMlEnabled } from '../common/license'; import { registerEmbeddables } from './embeddables'; diff --git a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx index e327befcf7293..e4d7cfa32c2cd 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_in_anomaly_explorer_action.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ActionContextMapping, createAction } from '../../../../../src/plugins/ui_actions/public'; import { MlCoreSetup } from '../plugin'; -import { ML_APP_URL_GENERATOR } from '../url_generator'; +import { ML_APP_URL_GENERATOR } from '../../common/constants/ml_url_generator'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, SwimLaneDrilldownContext } from '../embeddables'; export const OPEN_IN_ANOMALY_EXPLORER_ACTION = 'openInAnomalyExplorerAction'; @@ -32,19 +32,21 @@ export function createOpenInExplorerAction(getStartServices: MlCoreSetup['getSta return urlGenerator.createUrl({ page: 'explorer', - jobIds, - timeRange, - mlExplorerSwimlane: { - viewByFromPage: fromPage, - viewByPerPage: perPage, - viewByFieldName: viewBy, - ...(data - ? { - selectedType: data.type, - selectedTimes: data.times, - selectedLanes: data.lanes, - } - : {}), + pageState: { + jobIds, + timeRange, + mlExplorerSwimlane: { + viewByFromPage: fromPage, + viewByPerPage: perPage, + viewByFieldName: viewBy, + ...(data + ? { + selectedType: data.type, + selectedTimes: data.times, + selectedLanes: data.lanes, + } + : {}), + }, }, }); }, diff --git a/x-pack/plugins/ml/public/url_generator.test.ts b/x-pack/plugins/ml/public/url_generator.test.ts deleted file mode 100644 index 21dde12404957..0000000000000 --- a/x-pack/plugins/ml/public/url_generator.test.ts +++ /dev/null @@ -1,34 +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 { MlUrlGenerator } from './url_generator'; - -describe('MlUrlGenerator', () => { - const urlGenerator = new MlUrlGenerator({ - appBasePath: '/app/ml', - useHash: false, - }); - - it('should generate valid URL for the Anomaly Explorer page', async () => { - const url = await urlGenerator.createUrl({ - page: 'explorer', - jobIds: ['test-job'], - mlExplorerSwimlane: { viewByFromPage: 2, viewByPerPage: 20 }, - }); - expect(url).toBe( - '/app/ml/explorer#?_g=(ml:(jobIds:!(test-job)))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(viewByFromPage:2,viewByPerPage:20))' - ); - }); - - it('should throw an error in case the page is not provided', async () => { - expect.assertions(1); - - // @ts-ignore - await urlGenerator.createUrl({ jobIds: ['test-job'] }).catch((e) => { - expect(e.message).toEqual('Page type is not provided or unknown'); - }); - }); -}); diff --git a/x-pack/plugins/ml/public/url_generator.ts b/x-pack/plugins/ml/public/url_generator.ts deleted file mode 100644 index 4e08c57c0b2e0..0000000000000 --- a/x-pack/plugins/ml/public/url_generator.ts +++ /dev/null @@ -1,118 +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 { CoreSetup } from 'kibana/public'; -import { - SharePluginSetup, - UrlGeneratorsDefinition, - UrlGeneratorState, -} from '../../../../src/plugins/share/public'; -import { TimeRange } from '../../../../src/plugins/data/public'; -import { setStateToKbnUrl } from '../../../../src/plugins/kibana_utils/public'; -import { JobId } from '../../reporting/common/types'; -import { ExplorerAppState } from './application/explorer/explorer_dashboard_service'; -import { MlStartDependencies } from './plugin'; - -declare module '../../../../src/plugins/share/public' { - export interface UrlGeneratorStateMapping { - [ML_APP_URL_GENERATOR]: UrlGeneratorState; - } -} - -export const ML_APP_URL_GENERATOR = 'ML_APP_URL_GENERATOR'; - -export interface ExplorerUrlState { - /** - * ML App Page - */ - page: 'explorer'; - /** - * Job IDs - */ - jobIds: JobId[]; - /** - * Optionally set the time range in the time picker. - */ - timeRange?: TimeRange; - /** - * Optional state for the swim lane - */ - mlExplorerSwimlane?: ExplorerAppState['mlExplorerSwimlane']; - mlExplorerFilter?: ExplorerAppState['mlExplorerFilter']; -} - -/** - * Union type of ML URL state based on page - */ -export type MlUrlGeneratorState = ExplorerUrlState; - -export interface ExplorerQueryState { - ml: { jobIds: JobId[] }; - time?: TimeRange; -} - -interface Params { - appBasePath: string; - useHash: boolean; -} - -export class MlUrlGenerator implements UrlGeneratorsDefinition { - constructor(private readonly params: Params) {} - - public readonly id = ML_APP_URL_GENERATOR; - - public readonly createUrl = async ({ page, ...params }: MlUrlGeneratorState): Promise => { - if (page === 'explorer') { - return this.createExplorerUrl(params); - } - throw new Error('Page type is not provided or unknown'); - }; - - /** - * Creates URL to the Anomaly Explorer page - */ - private createExplorerUrl({ - timeRange, - jobIds, - mlExplorerSwimlane = {}, - mlExplorerFilter = {}, - }: Omit): string { - const appState: ExplorerAppState = { - mlExplorerSwimlane, - mlExplorerFilter, - }; - - const queryState: ExplorerQueryState = { - ml: { - jobIds, - }, - }; - - if (timeRange) queryState.time = timeRange; - - let url = `${this.params.appBasePath}/explorer`; - url = setStateToKbnUrl('_g', queryState, { useHash: false }, url); - url = setStateToKbnUrl('_a', appState, { useHash: false }, url); - - return url; - } -} - -/** - * Registers the URL generator - */ -export function registerUrlGenerator( - share: SharePluginSetup, - core: CoreSetup -) { - const baseUrl = core.http.basePath.prepend('/app/ml'); - share.urlGenerators.registerUrlGenerator( - new MlUrlGenerator({ - appBasePath: baseUrl, - useHash: core.uiSettings.get('state:storeInSessionStorage'), - }) - ); -} diff --git a/x-pack/plugins/ml/server/client/elasticsearch_ml.test.ts b/x-pack/plugins/ml/server/client/elasticsearch_ml.test.ts deleted file mode 100644 index 5ad0db3c58ce4..0000000000000 --- a/x-pack/plugins/ml/server/client/elasticsearch_ml.test.ts +++ /dev/null @@ -1,60 +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 { elasticsearchJsPlugin } from './elasticsearch_ml'; - -interface Endpoint { - fmt: string; -} - -interface ClientAction { - urls?: Endpoint[]; - url: Endpoint; -} - -describe('ML - Endpoints', () => { - // Check all paths in the ML elasticsearchJsPlugin start with a leading forward slash - // so they work if Kibana is run behind a reverse proxy - const PATH_START: string = '/'; - const urls: string[] = []; - - // Stub objects - const Client = { - prototype: {}, - }; - - const components = { - clientAction: { - factory(obj: ClientAction) { - // add each endpoint URL to a list - if (obj.urls) { - obj.urls.forEach((url) => { - urls.push(url.fmt); - }); - } - if (obj.url) { - urls.push(obj.url.fmt); - } - }, - namespaceFactory() { - return { - prototype: {}, - }; - }, - }, - }; - - // Stub elasticsearchJsPlugin - elasticsearchJsPlugin(Client, null, components); - - describe('paths', () => { - it(`should start with ${PATH_START}`, () => { - urls.forEach((url) => { - expect(url[0]).toEqual(PATH_START); - }); - }); - }); -}); diff --git a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts b/x-pack/plugins/ml/server/client/elasticsearch_ml.ts deleted file mode 100644 index 63153d18cb10b..0000000000000 --- a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts +++ /dev/null @@ -1,929 +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 const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { - const ca = components.clientAction.factory; - - Client.prototype.ml = components.clientAction.namespaceFactory(); - const ml = Client.prototype.ml.prototype; - - /** - * Perform a [ml.authenticate](Retrieve details about the currently authenticated user) request - * - * @param {Object} params - An object with parameters used to carry out this action - */ - ml.jobs = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>', - req: { - jobId: { - type: 'list', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/', - }, - ], - method: 'GET', - }); - - ml.jobStats = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_stats', - req: { - jobId: { - type: 'list', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/_stats', - }, - ], - method: 'GET', - }); - - ml.addJob = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - ml.openJob = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_open', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ml.closeJob = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_close?force=<%=force%>', - req: { - jobId: { - type: 'string', - }, - force: { - type: 'boolean', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_close', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - // Currently the endpoint uses a default size of 100 unless a size is supplied. - // So until paging is supported in the UI, explicitly supply a size of 1000 - // to match the max number of docs that the endpoint can return. - ml.getDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>', - req: { - analyticsId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/data_frame/analytics/_all?size=1000', - }, - ], - method: 'GET', - }); - - ml.getDataFrameAnalyticsStats = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_stats', - req: { - analyticsId: { - type: 'string', - }, - }, - }, - { - // Currently the endpoint uses a default size of 100 unless a size is supplied. - // So until paging is supported in the UI, explicitly supply a size of 1000 - // to match the max number of docs that the endpoint can return. - fmt: '/_ml/data_frame/analytics/_all/_stats?size=1000', - }, - ], - method: 'GET', - }); - - ml.createDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>', - req: { - analyticsId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - ml.evaluateDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/_evaluate', - }, - ], - needBody: true, - method: 'POST', - }); - - ml.explainDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/_explain', - }, - ], - needBody: true, - method: 'POST', - }); - - ml.deleteDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>', - req: { - analyticsId: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - ml.startDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_start', - req: { - analyticsId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ml.stopDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_stop?&force=<%=force%>', - req: { - analyticsId: { - type: 'string', - }, - force: { - type: 'boolean', - }, - }, - }, - ], - method: 'POST', - }); - - ml.updateDataFrameAnalytics = ca({ - urls: [ - { - fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_update', - req: { - analyticsId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - ml.deleteJob = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>?&force=<%=force%>&wait_for_completion=false', - req: { - jobId: { - type: 'string', - }, - force: { - type: 'boolean', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>?&wait_for_completion=false', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - ml.updateJob = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_update', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - ml.datafeeds = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>', - req: { - datafeedId: { - type: 'list', - }, - }, - }, - { - fmt: '/_ml/datafeeds/', - }, - ], - method: 'GET', - }); - - ml.datafeedStats = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_stats', - req: { - datafeedId: { - type: 'list', - }, - }, - }, - { - fmt: '/_ml/datafeeds/_stats', - }, - ], - method: 'GET', - }); - - ml.addDatafeed = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>', - req: { - datafeedId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - ml.updateDatafeed = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_update', - req: { - datafeedId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - ml.deleteDatafeed = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>?force=<%=force%>', - req: { - datafeedId: { - type: 'string', - }, - force: { - type: 'boolean', - }, - }, - }, - { - fmt: '/_ml/datafeeds/<%=datafeedId%>', - req: { - datafeedId: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - ml.startDatafeed = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_start?&start=<%=start%>&end=<%=end%>', - req: { - datafeedId: { - type: 'string', - }, - start: { - type: 'string', - }, - end: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_start?&start=<%=start%>', - req: { - datafeedId: { - type: 'string', - }, - start: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_start', - req: { - datafeedId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ml.stopDatafeed = ca({ - urls: [ - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_stop?force=<%=force%>', - req: { - datafeedId: { - type: 'string', - }, - force: { - type: 'boolean', - }, - }, - }, - { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_stop', - req: { - datafeedId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ml.validateDetector = ca({ - url: { - fmt: '/_ml/anomaly_detectors/_validate/detector', - }, - needBody: true, - method: 'POST', - }); - - ml.estimateModelMemory = ca({ - url: { - fmt: '/_ml/anomaly_detectors/_estimate_model_memory', - }, - needBody: true, - method: 'POST', - }); - - ml.datafeedPreview = ca({ - url: { - fmt: '/_ml/datafeeds/<%=datafeedId%>/_preview', - req: { - datafeedId: { - type: 'string', - }, - }, - }, - method: 'GET', - }); - - ml.forecast = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_forecast?&duration=<%=duration%>', - req: { - jobId: { - type: 'string', - }, - duration: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/_forecast', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ml.records = ca({ - url: { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/records', - req: { - jobId: { - type: 'string', - }, - }, - }, - method: 'POST', - }); - - ml.buckets = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/buckets', - req: { - jobId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/buckets/<%=timestamp%>', - req: { - jobId: { - type: 'string', - }, - timestamp: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ml.overallBuckets = ca({ - url: { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/overall_buckets', - req: { - jobId: { - type: 'string', - }, - }, - }, - method: 'POST', - }); - - ml.privilegeCheck = ca({ - url: { - fmt: '/_security/user/_has_privileges', - }, - needBody: true, - method: 'POST', - }); - // Currently the endpoint uses a default size of 100 unless a size is supplied. So until paging is supported in the UI, explicitly supply a size of 1000 - ml.calendars = ca({ - urls: [ - { - fmt: '/_ml/calendars/<%=calendarId%>', - req: { - calendarId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/calendars?size=1000', - }, - ], - method: 'GET', - }); - - ml.deleteCalendar = ca({ - url: { - fmt: '/_ml/calendars/<%=calendarId%>', - req: { - calendarId: { - type: 'string', - }, - }, - }, - method: 'DELETE', - }); - - ml.addCalendar = ca({ - url: { - fmt: '/_ml/calendars/<%=calendarId%>', - req: { - calendarId: { - type: 'string', - }, - }, - }, - needBody: true, - method: 'PUT', - }); - - ml.addJobToCalendar = ca({ - url: { - fmt: '/_ml/calendars/<%=calendarId%>/jobs/<%=jobId%>', - req: { - calendarId: { - type: 'string', - }, - jobId: { - type: 'string', - }, - }, - }, - method: 'PUT', - }); - - ml.removeJobFromCalendar = ca({ - url: { - fmt: '/_ml/calendars/<%=calendarId%>/jobs/<%=jobId%>', - req: { - calendarId: { - type: 'string', - }, - jobId: { - type: 'string', - }, - }, - }, - method: 'DELETE', - }); - - ml.events = ca({ - urls: [ - { - fmt: '/_ml/calendars/<%=calendarId%>/events', - req: { - calendarId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/calendars/<%=calendarId%>/events?&job_id=<%=jobId%>', - req: { - calendarId: { - type: 'string', - }, - jobId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/calendars/<%=calendarId%>/events?&after=<%=start%>&before=<%=end%>', - req: { - calendarId: { - type: 'string', - }, - start: { - type: 'string', - }, - end: { - type: 'string', - }, - }, - }, - { - fmt: - '/_ml/calendars/<%=calendarId%>/events?&after=<%=start%>&before=<%=end%>&job_id=<%=jobId%>', - req: { - calendarId: { - type: 'string', - }, - start: { - type: 'string', - }, - end: { - type: 'string', - }, - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ml.addEvent = ca({ - url: { - fmt: '/_ml/calendars/<%=calendarId%>/events', - req: { - calendarId: { - type: 'string', - }, - }, - }, - needBody: true, - method: 'POST', - }); - - ml.deleteEvent = ca({ - url: { - fmt: '/_ml/calendars/<%=calendarId%>/events/<%=eventId%>', - req: { - calendarId: { - type: 'string', - }, - eventId: { - type: 'string', - }, - }, - }, - method: 'DELETE', - }); - // Currently the endpoint uses a default size of 100 unless a size is supplied. So until paging is supported in the UI, explicitly supply a size of 1000 - ml.filters = ca({ - urls: [ - { - fmt: '/_ml/filters/<%=filterId%>', - req: { - filterId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/filters?size=1000', - }, - ], - method: 'GET', - }); - - ml.addFilter = ca({ - url: { - fmt: '/_ml/filters/<%=filterId%>', - req: { - filterId: { - type: 'string', - }, - }, - }, - needBody: true, - method: 'PUT', - }); - - ml.updateFilter = ca({ - urls: [ - { - fmt: '/_ml/filters/<%=filterId%>/_update', - req: { - filterId: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - ml.deleteFilter = ca({ - url: { - fmt: '/_ml/filters/<%=filterId%>', - req: { - filterId: { - type: 'string', - }, - }, - }, - method: 'DELETE', - }); - - ml.info = ca({ - url: { - fmt: '/_ml/info', - }, - method: 'GET', - }); - - ml.fileStructure = ca({ - urls: [ - { - fmt: - '/_ml/find_file_structure?&explain=true&charset=<%=charset%>&format=<%=format%>&has_header_row=<%=has_header_row%>&column_names=<%=column_names%>&delimiter=<%=delimiter%>"e=<%=quote%>&should_trim_fields=<%=should_trim_fields%>&grok_pattern=<%=grok_pattern%>×tamp_field=<%=timestamp_field%>×tamp_format=<%=timestamp_format%>&lines_to_sample=<%=lines_to_sample%>', - req: { - charset: { - type: 'string', - }, - format: { - type: 'string', - }, - has_header_row: { - type: 'string', - }, - column_names: { - type: 'string', - }, - delimiter: { - type: 'string', - }, - quote: { - type: 'string', - }, - should_trim_fields: { - type: 'string', - }, - grok_pattern: { - type: 'string', - }, - timestamp_field: { - type: 'string', - }, - timestamp_format: { - type: 'string', - }, - lines_to_sample: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/find_file_structure?&explain=true', - }, - ], - needBody: true, - method: 'POST', - }); - - ml.rollupIndexCapabilities = ca({ - urls: [ - { - fmt: '/<%=indexPattern%>/_rollup/data', - req: { - indexPattern: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ml.categories = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/categories/<%=categoryId%>', - req: { - jobId: { - type: 'string', - }, - categoryId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/results/categories', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ml.modelSnapshots = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>', - req: { - jobId: { - type: 'string', - }, - snapshotId: { - type: 'string', - }, - }, - }, - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/model_snapshots', - req: { - jobId: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ml.updateModelSnapshot = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>/_update', - req: { - jobId: { - type: 'string', - }, - snapshotId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - needBody: true, - }); - - ml.deleteModelSnapshot = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>', - req: { - jobId: { - type: 'string', - }, - snapshotId: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - ml.revertModelSnapshot = ca({ - urls: [ - { - fmt: '/_ml/anomaly_detectors/<%=jobId%>/model_snapshots/<%=snapshotId%>/_revert', - req: { - jobId: { - type: 'string', - }, - snapshotId: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); -}; diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index 21d32813c0d51..4dd17f8cf4889 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { getAdminCapabilities, getUserCapabilities } from './__mocks__/ml_capabilities'; import { capabilitiesProvider } from './check_capabilities'; import { MlLicense } from '../../../common/license'; @@ -24,16 +24,28 @@ const mlIsEnabled = async () => true; const mlIsNotEnabled = async () => false; const mlClusterClientNonUpgrade = ({ - callAsInternalUser: async () => ({ - upgrade_mode: false, - }), -} as unknown) as ILegacyScopedClusterClient; + asInternalUser: { + ml: { + info: async () => ({ + body: { + upgrade_mode: false, + }, + }), + }, + }, +} as unknown) as IScopedClusterClient; const mlClusterClientUpgrade = ({ - callAsInternalUser: async () => ({ - upgrade_mode: true, - }), -} as unknown) as ILegacyScopedClusterClient; + asInternalUser: { + ml: { + info: async () => ({ + body: { + upgrade_mode: true, + }, + }), + }, + }, +} as unknown) as IScopedClusterClient; describe('check_capabilities', () => { describe('getCapabilities() - right number of capabilities', () => { diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts index c976ab598b28c..c591ec07c7c3b 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient, KibanaRequest } from 'kibana/server'; +import { IScopedClusterClient, KibanaRequest } from 'kibana/server'; import { mlLog } from '../../client/log'; import { MlCapabilities, @@ -22,12 +22,12 @@ import { } from './errors'; export function capabilitiesProvider( - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, capabilities: MlCapabilities, mlLicense: MlLicense, isMlEnabledInSpace: () => Promise ) { - const { isUpgradeInProgress } = upgradeCheckProvider(mlClusterClient); + const { isUpgradeInProgress } = upgradeCheckProvider(client); async function getCapabilities(): Promise { const upgradeInProgress = await isUpgradeInProgress(); const isPlatinumOrTrialLicense = mlLicense.isFullLicense(); diff --git a/x-pack/plugins/ml/server/lib/capabilities/upgrade.ts b/x-pack/plugins/ml/server/lib/capabilities/upgrade.ts index 6df4d0c87ecf5..defb70429fa0c 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/upgrade.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/upgrade.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { mlLog } from '../../client/log'; -export function upgradeCheckProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { +export function upgradeCheckProvider({ asInternalUser }: IScopedClusterClient) { async function isUpgradeInProgress(): Promise { let upgradeInProgress = false; try { - const info = await callAsInternalUser('ml.info'); + const { body } = await asInternalUser.ml.info(); // if ml indices are currently being migrated, upgrade_mode will be set to true // pass this back with the privileges to allow for the disabling of UI controls. - upgradeInProgress = info.upgrade_mode === true; + upgradeInProgress = body.upgrade_mode === true; } catch (error) { // if the ml.info check fails, it could be due to the user having insufficient privileges // most likely they do not have the ml_user role and therefore will be blocked from using diff --git a/x-pack/plugins/ml/server/lib/check_annotations/index.ts b/x-pack/plugins/ml/server/lib/check_annotations/index.ts index de19f0ead6791..964f9d0b92261 100644 --- a/x-pack/plugins/ml/server/lib/check_annotations/index.ts +++ b/x-pack/plugins/ml/server/lib/check_annotations/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { mlLog } from '../../client/log'; import { @@ -17,18 +17,16 @@ import { // - ML_ANNOTATIONS_INDEX_PATTERN index is present // - ML_ANNOTATIONS_INDEX_ALIAS_READ alias is present // - ML_ANNOTATIONS_INDEX_ALIAS_WRITE alias is present -export async function isAnnotationsFeatureAvailable({ - callAsInternalUser, -}: ILegacyScopedClusterClient) { +export async function isAnnotationsFeatureAvailable({ asInternalUser }: IScopedClusterClient) { try { const indexParams = { index: ML_ANNOTATIONS_INDEX_PATTERN }; - const annotationsIndexExists = await callAsInternalUser('indices.exists', indexParams); + const { body: annotationsIndexExists } = await asInternalUser.indices.exists(indexParams); if (!annotationsIndexExists) { return false; } - const annotationsReadAliasExists = await callAsInternalUser('indices.existsAlias', { + const { body: annotationsReadAliasExists } = await asInternalUser.indices.existsAlias({ index: ML_ANNOTATIONS_INDEX_ALIAS_READ, name: ML_ANNOTATIONS_INDEX_ALIAS_READ, }); @@ -37,7 +35,7 @@ export async function isAnnotationsFeatureAvailable({ return false; } - const annotationsWriteAliasExists = await callAsInternalUser('indices.existsAlias', { + const { body: annotationsWriteAliasExists } = await asInternalUser.indices.existsAlias({ index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, name: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, }); diff --git a/x-pack/plugins/ml/server/lib/license/ml_server_license.ts b/x-pack/plugins/ml/server/lib/license/ml_server_license.ts index bd0a29721248a..6e3019b303b88 100644 --- a/x-pack/plugins/ml/server/lib/license/ml_server_license.ts +++ b/x-pack/plugins/ml/server/lib/license/ml_server_license.ts @@ -7,7 +7,6 @@ import { KibanaRequest, KibanaResponseFactory, RequestHandlerContext, - ILegacyScopedClusterClient, IScopedClusterClient, RequestHandler, } from 'kibana/server'; @@ -15,7 +14,6 @@ import { import { MlLicense } from '../../../common/license'; type Handler = (handlerParams: { - legacyClient: ILegacyScopedClusterClient; client: IScopedClusterClient; request: KibanaRequest; response: KibanaResponseFactory; @@ -42,7 +40,6 @@ function guard(check: () => boolean, handler: Handler) { } return handler({ - legacyClient: context.ml!.mlClient, client: context.core.elasticsearch.client, request, response, diff --git a/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts b/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts index 902cc907a0eff..28f12ead7924b 100644 --- a/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts +++ b/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts @@ -18,7 +18,7 @@ export function initSampleDataSets(mlLicense: MlLicense, plugins: PluginsSetup) addAppLinksToSampleDataset('ecommerce', [ { path: - '/app/ml#/modules/check_view_or_create?id=sample_data_ecommerce&index=ff959d40-b880-11e8-a6d9-e546fe2bba5f', + '/app/ml/modules/check_view_or_create?id=sample_data_ecommerce&index=ff959d40-b880-11e8-a6d9-e546fe2bba5f', label: sampleDataLinkLabel, icon: 'machineLearningApp', }, @@ -27,7 +27,7 @@ export function initSampleDataSets(mlLicense: MlLicense, plugins: PluginsSetup) addAppLinksToSampleDataset('logs', [ { path: - '/app/ml#/modules/check_view_or_create?id=sample_data_weblogs&index=90943e30-9a47-11e8-b64d-95841ca0b247', + '/app/ml/modules/check_view_or_create?id=sample_data_weblogs&index=90943e30-9a47-11e8-b64d-95841ca0b247', label: sampleDataLinkLabel, icon: 'machineLearningApp', }, diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts index 5be443266ffe1..4c511b567615d 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts @@ -22,19 +22,15 @@ describe('annotation_service', () => { let mlClusterClientSpy = {} as any; beforeEach(() => { - const callAs = jest.fn((action: string) => { - switch (action) { - case 'delete': - case 'index': - return Promise.resolve(acknowledgedResponseMock); - case 'search': - return Promise.resolve(getAnnotationsResponseMock); - } - }); + const callAs = { + delete: jest.fn(() => Promise.resolve({ body: acknowledgedResponseMock })), + index: jest.fn(() => Promise.resolve({ body: acknowledgedResponseMock })), + search: jest.fn(() => Promise.resolve({ body: getAnnotationsResponseMock })), + }; mlClusterClientSpy = { - callAsCurrentUser: callAs, - callAsInternalUser: callAs, + asCurrentUser: callAs, + asInternalUser: callAs, }; }); @@ -52,8 +48,7 @@ describe('annotation_service', () => { const response = await deleteAnnotation(annotationMockId); - expect(mockFunct.callAsInternalUser.mock.calls[0][0]).toBe('delete'); - expect(mockFunct.callAsInternalUser.mock.calls[0][1]).toEqual(deleteParamsMock); + expect(mockFunct.asInternalUser.delete.mock.calls[0][0]).toStrictEqual(deleteParamsMock); expect(response).toBe(acknowledgedResponseMock); done(); }); @@ -73,8 +68,9 @@ describe('annotation_service', () => { const response: GetResponse = await getAnnotations(indexAnnotationArgsMock); - expect(mockFunct.callAsInternalUser.mock.calls[0][0]).toBe('search'); - expect(mockFunct.callAsInternalUser.mock.calls[0][1]).toEqual(getAnnotationsRequestMock); + expect(mockFunct.asInternalUser.search.mock.calls[0][0]).toStrictEqual( + getAnnotationsRequestMock + ); expect(Object.keys(response.annotations)).toHaveLength(1); expect(response.annotations[jobIdMock]).toHaveLength(2); expect(isAnnotations(response.annotations[jobIdMock])).toBeTruthy(); @@ -89,9 +85,9 @@ describe('annotation_service', () => { }; const mlClusterClientSpyError: any = { - callAsInternalUser: jest.fn(() => { - return Promise.resolve(mockEsError); - }), + asInternalUser: { + search: jest.fn(() => Promise.resolve({ body: mockEsError })), + }, }; const { getAnnotations } = annotationServiceProvider(mlClusterClientSpyError); @@ -124,10 +120,8 @@ describe('annotation_service', () => { const response = await indexAnnotation(annotationMock, usernameMock); - expect(mockFunct.callAsInternalUser.mock.calls[0][0]).toBe('index'); - // test if the annotation has been correctly augmented - const indexParamsCheck = mockFunct.callAsInternalUser.mock.calls[0][1]; + const indexParamsCheck = mockFunct.asInternalUser.index.mock.calls[0][0]; const annotation = indexParamsCheck.body; expect(annotation.create_username).toBe(usernameMock); expect(annotation.modified_username).toBe(usernameMock); @@ -154,10 +148,8 @@ describe('annotation_service', () => { const response = await indexAnnotation(annotationMock, usernameMock); - expect(mockFunct.callAsInternalUser.mock.calls[0][0]).toBe('index'); - // test if the annotation has been correctly augmented - const indexParamsCheck = mockFunct.callAsInternalUser.mock.calls[0][1]; + const indexParamsCheck = mockFunct.asInternalUser.index.mock.calls[0][0]; const annotation = indexParamsCheck.body; expect(annotation.create_username).toBe(usernameMock); expect(annotation.modified_username).toBe(usernameMock); @@ -196,9 +188,8 @@ describe('annotation_service', () => { await indexAnnotation(annotation, modifiedUsernameMock); - expect(mockFunct.callAsInternalUser.mock.calls[1][0]).toBe('index'); // test if the annotation has been correctly updated - const indexParamsCheck = mockFunct.callAsInternalUser.mock.calls[1][1]; + const indexParamsCheck = mockFunct.asInternalUser.index.mock.calls[0][0]; const modifiedAnnotation = indexParamsCheck.body; expect(modifiedAnnotation.annotation).toBe(modifiedAnnotationText); expect(modifiedAnnotation.create_username).toBe(originalUsernameMock); diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts index a585449db0a25..24f1d6951c940 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts @@ -7,7 +7,7 @@ import Boom from 'boom'; import each from 'lodash/each'; import get from 'lodash/get'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ANNOTATION_EVENT_USER, ANNOTATION_TYPE } from '../../../common/constants/annotations'; import { PARTITION_FIELDS } from '../../../common/constants/anomalies'; @@ -67,17 +67,17 @@ export interface GetResponse { export interface IndexParams { index: string; body: Annotation; - refresh?: string; + refresh: boolean | 'wait_for' | undefined; id?: string; } export interface DeleteParams { index: string; - refresh?: string; + refresh: boolean | 'wait_for' | undefined; id: string; } -export function annotationProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { +export function annotationProvider({ asInternalUser }: IScopedClusterClient) { async function indexAnnotation(annotation: Annotation, username: string) { if (isAnnotation(annotation) === false) { // No need to translate, this will not be exposed in the UI. @@ -104,7 +104,8 @@ export function annotationProvider({ callAsInternalUser }: ILegacyScopedClusterC delete params.body.key; } - return await callAsInternalUser('index', params); + const { body } = await asInternalUser.index(params); + return body; } async function getAnnotations({ @@ -287,14 +288,14 @@ export function annotationProvider({ callAsInternalUser }: ILegacyScopedClusterC }; try { - const resp = await callAsInternalUser('search', params); + const { body } = await asInternalUser.search(params); - if (resp.error !== undefined && resp.message !== undefined) { + if (body.error !== undefined && body.message !== undefined) { // No need to translate, this will not be exposed in the UI. throw new Error(`Annotations couldn't be retrieved from Elasticsearch.`); } - const docs: Annotations = get(resp, ['hits', 'hits'], []).map((d: EsResult) => { + const docs: Annotations = get(body, ['hits', 'hits'], []).map((d: EsResult) => { // get the original source document and the document id, we need it // to identify the annotation when editing/deleting it. // if original `event` is undefined then substitute with 'user` by default @@ -306,7 +307,7 @@ export function annotationProvider({ callAsInternalUser }: ILegacyScopedClusterC } as Annotation; }); - const aggregations = get(resp, ['aggregations'], {}) as EsAggregationResult; + const aggregations = get(body, ['aggregations'], {}) as EsAggregationResult; if (fields) { obj.aggregations = aggregations; } @@ -330,13 +331,14 @@ export function annotationProvider({ callAsInternalUser }: ILegacyScopedClusterC } async function deleteAnnotation(id: string) { - const param: DeleteParams = { + const params: DeleteParams = { index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, id, refresh: 'wait_for', }; - return await callAsInternalUser('delete', param); + const { body } = await asInternalUser.delete(params); + return body; } return { diff --git a/x-pack/plugins/ml/server/models/annotation_service/index.ts b/x-pack/plugins/ml/server/models/annotation_service/index.ts index e17af2a154b87..9fcb84e2938ae 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/index.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/index.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { annotationProvider } from './annotation'; -export function annotationServiceProvider(mlClusterClient: ILegacyScopedClusterClient) { +export function annotationServiceProvider(client: IScopedClusterClient) { return { - ...annotationProvider(mlClusterClient), + ...annotationProvider(client), }; } diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts index eeabb24d9be3b..5b52414d6753a 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ES_AGGREGATION } from '../../../common/constants/aggregation_types'; export interface BucketSpanEstimatorData { @@ -21,6 +21,6 @@ export interface BucketSpanEstimatorData { } export function estimateBucketSpanFactory({ - callAsCurrentUser, - callAsInternalUser, -}: ILegacyScopedClusterClient): (config: BucketSpanEstimatorData) => Promise; + asCurrentUser, + asInternalUser, +}: IScopedClusterClient): (config: BucketSpanEstimatorData) => Promise; diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js index 381c615051e3b..1d59db8fa564f 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js @@ -16,10 +16,10 @@ import { INTERVALS } from './intervals'; import { singleSeriesCheckerFactory } from './single_series_checker'; import { polledDataCheckerFactory } from './polled_data_checker'; -export function estimateBucketSpanFactory(mlClusterClient) { - const { callAsCurrentUser, callAsInternalUser } = mlClusterClient; - const PolledDataChecker = polledDataCheckerFactory(mlClusterClient); - const SingleSeriesChecker = singleSeriesCheckerFactory(mlClusterClient); +export function estimateBucketSpanFactory(client) { + const { asCurrentUser, asInternalUser } = client; + const PolledDataChecker = polledDataCheckerFactory(client); + const SingleSeriesChecker = singleSeriesCheckerFactory(client); class BucketSpanEstimator { constructor( @@ -246,21 +246,22 @@ export function estimateBucketSpanFactory(mlClusterClient) { const getFieldCardinality = function (index, field) { return new Promise((resolve, reject) => { - callAsCurrentUser('search', { - index, - size: 0, - body: { - aggs: { - field_count: { - cardinality: { - field, + asCurrentUser + .search({ + index, + size: 0, + body: { + aggs: { + field_count: { + cardinality: { + field, + }, }, }, }, - }, - }) - .then((resp) => { - const value = get(resp, ['aggregations', 'field_count', 'value'], 0); + }) + .then(({ body }) => { + const value = get(body, ['aggregations', 'field_count', 'value'], 0); resolve(value); }) .catch((resp) => { @@ -278,28 +279,29 @@ export function estimateBucketSpanFactory(mlClusterClient) { getFieldCardinality(index, field) .then((value) => { const numPartitions = Math.floor(value / NUM_PARTITIONS) || 1; - callAsCurrentUser('search', { - index, - size: 0, - body: { - query, - aggs: { - fields_bucket_counts: { - terms: { - field, - include: { - partition: 0, - num_partitions: numPartitions, + asCurrentUser + .search({ + index, + size: 0, + body: { + query, + aggs: { + fields_bucket_counts: { + terms: { + field, + include: { + partition: 0, + num_partitions: numPartitions, + }, }, }, }, }, - }, - }) - .then((partitionResp) => { + }) + .then(({ body }) => { // eslint-disable-next-line camelcase - if (partitionResp.aggregations?.fields_bucket_counts?.buckets !== undefined) { - const buckets = partitionResp.aggregations.fields_bucket_counts.buckets; + if (body.aggregations?.fields_bucket_counts?.buckets !== undefined) { + const buckets = body.aggregations.fields_bucket_counts.buckets; fieldValues = buckets.map((b) => b.key); } resolve(fieldValues); @@ -338,21 +340,21 @@ export function estimateBucketSpanFactory(mlClusterClient) { return new Promise((resolve, reject) => { // fetch the `search.max_buckets` cluster setting so we're able to // adjust aggregations to not exceed that limit. - callAsInternalUser('cluster.getSettings', { - flatSettings: true, - includeDefaults: true, - filterPath: '*.*max_buckets', - }) - .then((settings) => { - if (typeof settings !== 'object') { + asInternalUser.cluster + .getSettings({ + flat_settings: true, + include_defaults: true, + filter_path: '*.*max_buckets', + }) + .then(({ body }) => { + if (typeof body !== 'object') { reject('Unable to retrieve cluster settings'); } // search.max_buckets could exist in default, persistent or transient cluster settings - const maxBucketsSetting = (settings.defaults || - settings.persistent || - settings.transient || - {})['search.max_buckets']; + const maxBucketsSetting = (body.defaults || body.persistent || body.transient || {})[ + 'search.max_buckets' + ]; if (maxBucketsSetting === undefined) { reject('Unable to retrieve cluster setting search.max_buckets'); diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts index f7c7dd8172ea5..35c4f1a0a741b 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.test.ts @@ -4,22 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ES_AGGREGATION } from '../../../common/constants/aggregation_types'; import { estimateBucketSpanFactory, BucketSpanEstimatorData } from './bucket_span_estimator'; -const callAs = () => { - return new Promise((resolve) => { - resolve({}); - }) as Promise; +const callAs = { + search: () => Promise.resolve({ body: {} }), + cluster: { getSettings: () => Promise.resolve({ body: {} }) }, }; -const mlClusterClient: ILegacyScopedClusterClient = { - callAsCurrentUser: callAs, - callAsInternalUser: callAs, -}; +const mlClusterClient = ({ + asCurrentUser: callAs, + asInternalUser: callAs, +} as unknown) as IScopedClusterClient; // mock configuration to be passed to the estimator const formConfig: BucketSpanEstimatorData = { diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js index d3bbd59f3cf9b..fd0cab7c0625d 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js @@ -12,7 +12,7 @@ import get from 'lodash/get'; -export function polledDataCheckerFactory({ callAsCurrentUser }) { +export function polledDataCheckerFactory({ asCurrentUser }) { class PolledDataChecker { constructor(index, timeField, duration, query) { this.index = index; @@ -65,14 +65,15 @@ export function polledDataCheckerFactory({ callAsCurrentUser }) { return search; } - performSearch(intervalMs) { - const body = this.createSearch(intervalMs); + async performSearch(intervalMs) { + const searchBody = this.createSearch(intervalMs); - return callAsCurrentUser('search', { + const { body } = await asCurrentUser.search({ index: this.index, size: 0, - body, + body: searchBody, }); + return body; } // test that the coefficient of variation of time difference between non-empty buckets is small diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js index a5449395501dc..750f0cfc0b4a8 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js @@ -13,7 +13,7 @@ import { mlLog } from '../../client/log'; import { INTERVALS, LONG_INTERVALS } from './intervals'; -export function singleSeriesCheckerFactory({ callAsCurrentUser }) { +export function singleSeriesCheckerFactory({ asCurrentUser }) { const REF_DATA_INTERVAL = { name: '1h', ms: 3600000 }; class SingleSeriesChecker { @@ -184,14 +184,15 @@ export function singleSeriesCheckerFactory({ callAsCurrentUser }) { return search; } - performSearch(intervalMs) { - const body = this.createSearch(intervalMs); + async performSearch(intervalMs) { + const searchBody = this.createSearch(intervalMs); - return callAsCurrentUser('search', { + const { body } = await asCurrentUser.search({ index: this.index, size: 0, - body, + body: searchBody, }); + return body; } getFullBuckets(buckets) { diff --git a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts index bc3c326e7d070..2a2c30601e2ca 100644 --- a/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/calculate_model_memory_limit/calculate_model_memory_limit.ts @@ -5,13 +5,13 @@ */ import numeral from '@elastic/numeral'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { MLCATEGORY } from '../../../common/constants/field_types'; import { AnalysisConfig } from '../../../common/types/anomaly_detection_jobs'; import { fieldsServiceProvider } from '../fields_service'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; -interface ModelMemoryEstimationResult { +export interface ModelMemoryEstimationResult { /** * Result model memory limit */ @@ -29,15 +29,15 @@ interface ModelMemoryEstimationResult { /** * Response of the _estimate_model_memory endpoint. */ -export interface ModelMemoryEstimate { +export interface ModelMemoryEstimateResponse { model_memory_estimate: string; } /** * Retrieves overall and max bucket cardinalities. */ -const cardinalityCheckProvider = (mlClusterClient: ILegacyScopedClusterClient) => { - const fieldsService = fieldsServiceProvider(mlClusterClient); +const cardinalityCheckProvider = (client: IScopedClusterClient) => { + const fieldsService = fieldsServiceProvider(client); return async ( analysisConfig: AnalysisConfig, @@ -123,9 +123,9 @@ const cardinalityCheckProvider = (mlClusterClient: ILegacyScopedClusterClient) = }; }; -export function calculateModelMemoryLimitProvider(mlClusterClient: ILegacyScopedClusterClient) { - const { callAsInternalUser } = mlClusterClient; - const getCardinalities = cardinalityCheckProvider(mlClusterClient); +export function calculateModelMemoryLimitProvider(client: IScopedClusterClient) { + const { asInternalUser } = client; + const getCardinalities = cardinalityCheckProvider(client); /** * Retrieves an estimated size of the model memory limit used in the job config @@ -141,7 +141,7 @@ export function calculateModelMemoryLimitProvider(mlClusterClient: ILegacyScoped latestMs: number, allowMMLGreaterThanMax = false ): Promise { - const info = (await callAsInternalUser('ml.info')) as MlInfoResponse; + const { body: info } = await asInternalUser.ml.info(); const maxModelMemoryLimit = info.limits.max_model_memory_limit?.toUpperCase(); const effectiveMaxModelMemoryLimit = info.limits.effective_max_model_memory_limit?.toUpperCase(); @@ -154,13 +154,14 @@ export function calculateModelMemoryLimitProvider(mlClusterClient: ILegacyScoped latestMs ); - const estimatedModelMemoryLimit = ((await callAsInternalUser('ml.estimateModelMemory', { + const { body } = await asInternalUser.ml.estimateModelMemory({ body: { analysis_config: analysisConfig, overall_cardinality: overallCardinality, max_bucket_cardinality: maxBucketCardinality, }, - })) as ModelMemoryEstimate).model_memory_estimate.toUpperCase(); + }); + const estimatedModelMemoryLimit = body.model_memory_estimate.toUpperCase(); let modelMemoryLimit = estimatedModelMemoryLimit; let mmlCappedAtMax = false; diff --git a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts index b461e71c006ff..95684e790636f 100644 --- a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts @@ -5,7 +5,7 @@ */ import { difference } from 'lodash'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { EventManager, CalendarEvent } from './event_manager'; interface BasicCalendar { @@ -23,30 +23,30 @@ export interface FormCalendar extends BasicCalendar { } export class CalendarManager { - private _callAsInternalUser: ILegacyScopedClusterClient['callAsInternalUser']; + private _asInternalUser: IScopedClusterClient['asInternalUser']; private _eventManager: EventManager; - constructor(mlClusterClient: ILegacyScopedClusterClient) { - this._callAsInternalUser = mlClusterClient.callAsInternalUser; - this._eventManager = new EventManager(mlClusterClient); + constructor(client: IScopedClusterClient) { + this._asInternalUser = client.asInternalUser; + this._eventManager = new EventManager(client); } async getCalendar(calendarId: string) { - const resp = await this._callAsInternalUser('ml.calendars', { - calendarId, + const { body } = await this._asInternalUser.ml.getCalendars({ + calendar_id: calendarId, }); - const calendars = resp.calendars; + const calendars = body.calendars; const calendar = calendars[0]; // Endpoint throws a 404 if calendar is not found. calendar.events = await this._eventManager.getCalendarEvents(calendarId); return calendar; } async getAllCalendars() { - const calendarsResp = await this._callAsInternalUser('ml.calendars'); + const { body } = await this._asInternalUser.ml.getCalendars({ size: 1000 }); const events: CalendarEvent[] = await this._eventManager.getAllEvents(); - const calendars: Calendar[] = calendarsResp.calendars; + const calendars: Calendar[] = body.calendars; calendars.forEach((cal) => (cal.events = [])); // loop events and combine with related calendars @@ -71,8 +71,8 @@ export class CalendarManager { async newCalendar(calendar: FormCalendar) { const { calendarId, events, ...newCalendar } = calendar; - await this._callAsInternalUser('ml.addCalendar', { - calendarId, + await this._asInternalUser.ml.putCalendar({ + calendar_id: calendarId, body: newCalendar, }); @@ -106,17 +106,17 @@ export class CalendarManager { // add all new jobs if (jobsToAdd.length) { - await this._callAsInternalUser('ml.addJobToCalendar', { - calendarId, - jobId: jobsToAdd.join(','), + await this._asInternalUser.ml.putCalendarJob({ + calendar_id: calendarId, + job_id: jobsToAdd.join(','), }); } // remove all removed jobs if (jobsToRemove.length) { - await this._callAsInternalUser('ml.removeJobFromCalendar', { - calendarId, - jobId: jobsToRemove.join(','), + await this._asInternalUser.ml.deleteCalendarJob({ + calendar_id: calendarId, + job_id: jobsToRemove.join(','), }); } @@ -137,6 +137,7 @@ export class CalendarManager { } async deleteCalendar(calendarId: string) { - return this._callAsInternalUser('ml.deleteCalendar', { calendarId }); + const { body } = await this._asInternalUser.ml.deleteCalendar({ calendar_id: calendarId }); + return body; } } diff --git a/x-pack/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/plugins/ml/server/models/calendar/event_manager.ts index b670bbe187544..b42068e748394 100644 --- a/x-pack/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/event_manager.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; export interface CalendarEvent { @@ -16,39 +16,42 @@ export interface CalendarEvent { } export class EventManager { - private _callAsInternalUser: ILegacyScopedClusterClient['callAsInternalUser']; - constructor({ callAsInternalUser }: ILegacyScopedClusterClient) { - this._callAsInternalUser = callAsInternalUser; + private _asInternalUser: IScopedClusterClient['asInternalUser']; + constructor({ asInternalUser }: IScopedClusterClient) { + this._asInternalUser = asInternalUser; } async getCalendarEvents(calendarId: string) { - const resp = await this._callAsInternalUser('ml.events', { calendarId }); + const { body } = await this._asInternalUser.ml.getCalendarEvents({ calendar_id: calendarId }); - return resp.events; + return body.events; } // jobId is optional async getAllEvents(jobId?: string) { const calendarId = GLOBAL_CALENDAR; - const resp = await this._callAsInternalUser('ml.events', { - calendarId, - jobId, + const { body } = await this._asInternalUser.ml.getCalendarEvents({ + calendar_id: calendarId, + job_id: jobId, }); - return resp.events; + return body.events; } async addEvents(calendarId: string, events: CalendarEvent[]) { const body = { events }; - return await this._callAsInternalUser('ml.addEvent', { - calendarId, + return await this._asInternalUser.ml.postCalendarEvents({ + calendar_id: calendarId, body, }); } async deleteEvent(calendarId: string, eventId: string) { - return this._callAsInternalUser('ml.deleteEvent', { calendarId, eventId }); + return this._asInternalUser.ml.deleteCalendarEvent({ + calendar_id: calendarId, + event_id: eventId, + }); } isEqual(ev1: CalendarEvent, ev2: CalendarEvent) { diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts index 1cb0656e88a0b..0f4cac37d2e8f 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { JobMessage } from '../../../common/types/audit_message'; @@ -23,7 +23,7 @@ interface BoolQuery { bool: { [key: string]: any }; } -export function analyticsAuditMessagesProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { +export function analyticsAuditMessagesProvider({ asInternalUser }: IScopedClusterClient) { // search for audit messages, // analyticsId is optional. without it, all analytics will be listed. async function getAnalyticsAuditMessages(analyticsId: string) { @@ -68,27 +68,23 @@ export function analyticsAuditMessagesProvider({ callAsInternalUser }: ILegacySc }); } - try { - const resp = await callAsInternalUser('search', { - index: ML_NOTIFICATION_INDEX_PATTERN, - ignore_unavailable: true, - rest_total_hits_as_int: true, - size: SIZE, - body: { - sort: [{ timestamp: { order: 'desc' } }, { job_id: { order: 'asc' } }], - query, - }, - }); + const { body } = await asInternalUser.search({ + index: ML_NOTIFICATION_INDEX_PATTERN, + ignore_unavailable: true, + rest_total_hits_as_int: true, + size: SIZE, + body: { + sort: [{ timestamp: { order: 'desc' } }, { job_id: { order: 'asc' } }], + query, + }, + }); - let messages = []; - if (resp.hits.total !== 0) { - messages = resp.hits.hits.map((hit: Message) => hit._source); - messages.reverse(); - } - return messages; - } catch (e) { - throw e; + let messages = []; + if (body.hits.total !== 0) { + messages = body.hits.hits.map((hit: Message) => hit._source); + messages.reverse(); } + return messages; } return { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts index 82d7707464308..ab879d4c93b29 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.test.ts @@ -4,13 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, KibanaRequest } from 'kibana/server'; +import { SavedObjectsClientContract, KibanaRequest, IScopedClusterClient } from 'kibana/server'; import { Module } from '../../../common/types/modules'; import { DataRecognizer } from '../data_recognizer'; +const callAs = () => Promise.resolve({ body: {} }); + +const mlClusterClient = ({ + asCurrentUser: callAs, + asInternalUser: callAs, +} as unknown) as IScopedClusterClient; + describe('ML - data recognizer', () => { const dr = new DataRecognizer( - { callAsCurrentUser: jest.fn(), callAsInternalUser: jest.fn() }, + mlClusterClient, ({ find: jest.fn(), bulkCreate: jest.fn(), diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 206baacd98322..820fcfa9253b6 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -7,15 +7,11 @@ import fs from 'fs'; import Boom from 'boom'; import numeral from '@elastic/numeral'; -import { - KibanaRequest, - ILegacyScopedClusterClient, - SavedObjectsClientContract, -} from 'kibana/server'; +import { KibanaRequest, IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; import moment from 'moment'; import { IndexPatternAttributes } from 'src/plugins/data/server'; import { merge } from 'lodash'; -import { AnalysisLimits, CombinedJobWithStats } from '../../../common/types/anomaly_detection_jobs'; +import { AnalysisLimits } from '../../../common/types/anomaly_detection_jobs'; import { getAuthorizationHeader } from '../../lib/request_authorization'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import { @@ -46,6 +42,7 @@ import { fieldsServiceProvider } from '../fields_service'; import { jobServiceProvider } from '../job_service'; import { resultsServiceProvider } from '../results_service'; import { JobExistResult, JobStat } from '../../../common/types/data_recognizer'; +import { MlJobsStatsResponse } from '../job_service/jobs'; const ML_DIR = 'ml'; const KIBANA_DIR = 'kibana'; @@ -74,10 +71,6 @@ interface RawModuleConfig { }; } -interface MlJobStats { - jobs: CombinedJobWithStats[]; -} - interface Config { dirName: any; json: RawModuleConfig; @@ -111,9 +104,9 @@ interface SaveResults { } export class DataRecognizer { - private _callAsCurrentUser: ILegacyScopedClusterClient['callAsCurrentUser']; - private _callAsInternalUser: ILegacyScopedClusterClient['callAsInternalUser']; - private _mlClusterClient: ILegacyScopedClusterClient; + private _asCurrentUser: IScopedClusterClient['asCurrentUser']; + private _asInternalUser: IScopedClusterClient['asInternalUser']; + private _client: IScopedClusterClient; private _authorizationHeader: object; private _modulesDir = `${__dirname}/modules`; private _indexPatternName: string = ''; @@ -124,13 +117,13 @@ export class DataRecognizer { jobsForModelMemoryEstimation: Array<{ job: ModuleJob; query: any }> = []; constructor( - mlClusterClient: ILegacyScopedClusterClient, + mlClusterClient: IScopedClusterClient, private savedObjectsClient: SavedObjectsClientContract, request: KibanaRequest ) { - this._mlClusterClient = mlClusterClient; - this._callAsCurrentUser = mlClusterClient.callAsCurrentUser; - this._callAsInternalUser = mlClusterClient.callAsInternalUser; + this._client = mlClusterClient; + this._asCurrentUser = mlClusterClient.asCurrentUser; + this._asInternalUser = mlClusterClient.asInternalUser; this._authorizationHeader = getAuthorizationHeader(request); } @@ -249,18 +242,18 @@ export class DataRecognizer { const index = indexPattern; const size = 0; - const body = { + const searchBody = { query: moduleConfig.query, }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, rest_total_hits_as_int: true, size, - body, + body: searchBody, }); - return resp.hits.total !== 0; + return body.hits.total !== 0; } async listModules() { @@ -518,7 +511,7 @@ export class DataRecognizer { // Add a wildcard at the front of each of the job IDs in the module, // as a prefix may have been supplied when creating the jobs in the module. const jobIds = module.jobs.map((job) => `*${job.id}`); - const { jobsExist } = jobServiceProvider(this._mlClusterClient); + const { jobsExist } = jobServiceProvider(this._client); const jobInfo = await jobsExist(jobIds); // Check if the value for any of the jobs is false. @@ -527,16 +520,16 @@ export class DataRecognizer { if (doJobsExist === true) { // Get the IDs of the jobs created from the module, and their earliest / latest timestamps. - const jobStats: MlJobStats = await this._callAsInternalUser('ml.jobStats', { - jobId: jobIds, + const { body } = await this._asInternalUser.ml.getJobStats({ + job_id: jobIds.join(), }); const jobStatsJobs: JobStat[] = []; - if (jobStats.jobs && jobStats.jobs.length > 0) { - const foundJobIds = jobStats.jobs.map((job) => job.job_id); - const { getLatestBucketTimestampByJob } = resultsServiceProvider(this._mlClusterClient); + if (body.jobs && body.jobs.length > 0) { + const foundJobIds = body.jobs.map((job) => job.job_id); + const { getLatestBucketTimestampByJob } = resultsServiceProvider(this._client); const latestBucketTimestampsByJob = await getLatestBucketTimestampByJob(foundJobIds); - jobStats.jobs.forEach((job) => { + body.jobs.forEach((job) => { const jobStat = { id: job.job_id, } as JobStat; @@ -704,16 +697,15 @@ export class DataRecognizer { job.id = jobId; await this.saveJob(job); return { id: jobId, success: true }; - } catch (error) { - return { id: jobId, success: false, error }; + } catch ({ body }) { + return { id: jobId, success: false, error: body }; } }) ); } async saveJob(job: ModuleJob) { - const { id: jobId, config: body } = job; - return this._callAsInternalUser('ml.addJob', { jobId, body }); + return this._asInternalUser.ml.putJob({ job_id: job.id, body: job.config }); } // save the datafeeds. @@ -725,20 +717,21 @@ export class DataRecognizer { try { await this.saveDatafeed(datafeed); return { id: datafeed.id, success: true, started: false }; - } catch (error) { - return { id: datafeed.id, success: false, started: false, error }; + } catch ({ body }) { + return { id: datafeed.id, success: false, started: false, error: body }; } }) ); } async saveDatafeed(datafeed: ModuleDataFeed) { - const { id: datafeedId, config: body } = datafeed; - return this._callAsInternalUser('ml.addDatafeed', { - datafeedId, - body, - ...this._authorizationHeader, - }); + return this._asInternalUser.ml.putDatafeed( + { + datafeed_id: datafeed.id, + body: datafeed.config, + }, + this._authorizationHeader + ); } async startDatafeeds( @@ -761,10 +754,10 @@ export class DataRecognizer { const result = { started: false } as DatafeedResponse; let opened = false; try { - const openResult = await this._callAsInternalUser('ml.openJob', { - jobId: datafeed.config.job_id, + const { body } = await this._asInternalUser.ml.openJob({ + job_id: datafeed.config.job_id, }); - opened = openResult.opened; + opened = body.opened; } catch (error) { // if the job is already open, a 409 will be returned. if (error.statusCode === 409) { @@ -772,27 +765,27 @@ export class DataRecognizer { } else { opened = false; result.started = false; - result.error = error; + result.error = error.body; } } if (opened) { try { - const duration: { start: number; end?: number } = { start: 0 }; + const duration: { start: string; end?: string } = { start: '0' }; if (start !== undefined) { - duration.start = start; + duration.start = (start as unknown) as string; } if (end !== undefined) { - duration.end = end; + duration.end = (end as unknown) as string; } - await this._callAsInternalUser('ml.startDatafeed', { - datafeedId: datafeed.id, + await this._asInternalUser.ml.startDatafeed({ + datafeed_id: datafeed.id, ...duration, }); result.started = true; - } catch (error) { + } catch ({ body }) { result.started = false; - result.error = error; + result.error = body; } } return result; @@ -995,7 +988,7 @@ export class DataRecognizer { timeField: string, query?: any ): Promise<{ start: number; end: number }> { - const fieldsService = fieldsServiceProvider(this._mlClusterClient); + const fieldsService = fieldsServiceProvider(this._client); const timeFieldRange = await fieldsService.getTimeFieldRange( this._indexPatternName, @@ -1025,7 +1018,7 @@ export class DataRecognizer { if (estimateMML && this.jobsForModelMemoryEstimation.length > 0) { try { - const calculateModelMemoryLimit = calculateModelMemoryLimitProvider(this._mlClusterClient); + const calculateModelMemoryLimit = calculateModelMemoryLimitProvider(this._client); // Checks if all jobs in the module have the same time field configured const firstJobTimeField = this.jobsForModelMemoryEstimation[0].job.config.data_description @@ -1074,11 +1067,13 @@ export class DataRecognizer { job.config.analysis_limits.model_memory_limit = modelMemoryLimit; } } catch (error) { - mlLog.warn(`Data recognizer could not estimate model memory limit ${error}`); + mlLog.warn(`Data recognizer could not estimate model memory limit ${error.body}`); } } - const { limits } = (await this._callAsInternalUser('ml.info')) as MlInfoResponse; + const { + body: { limits }, + } = await this._asInternalUser.ml.info(); const maxMml = limits.max_model_memory_limit; if (!maxMml) { diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index 838315d8d272c..dbfa4b5656e5f 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import get from 'lodash/get'; import each from 'lodash/each'; import last from 'lodash/last'; @@ -183,7 +183,7 @@ type BatchStats = | FieldExamples; const getAggIntervals = async ( - { callAsCurrentUser }: ILegacyScopedClusterClient, + { asCurrentUser }: IScopedClusterClient, indexPatternTitle: string, query: any, fields: HistogramField[], @@ -207,7 +207,7 @@ const getAggIntervals = async ( return aggs; }, {} as Record); - const respStats = await callAsCurrentUser('search', { + const { body } = await asCurrentUser.search({ index: indexPatternTitle, size: 0, body: { @@ -218,8 +218,7 @@ const getAggIntervals = async ( }); const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const aggregations = - aggsPath.length > 0 ? get(respStats.aggregations, aggsPath) : respStats.aggregations; + const aggregations = aggsPath.length > 0 ? get(body.aggregations, aggsPath) : body.aggregations; return Object.keys(aggregations).reduce((p, aggName) => { const stats = [aggregations[aggName].min, aggregations[aggName].max]; @@ -241,15 +240,15 @@ const getAggIntervals = async ( // export for re-use by transforms plugin export const getHistogramsForFields = async ( - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, indexPatternTitle: string, query: any, fields: HistogramField[], samplerShardSize: number ) => { - const { callAsCurrentUser } = mlClusterClient; + const { asCurrentUser } = client; const aggIntervals = await getAggIntervals( - mlClusterClient, + client, indexPatternTitle, query, fields, @@ -291,7 +290,7 @@ export const getHistogramsForFields = async ( return []; } - const respChartsData = await callAsCurrentUser('search', { + const { body } = await asCurrentUser.search({ index: indexPatternTitle, size: 0, body: { @@ -302,8 +301,7 @@ export const getHistogramsForFields = async ( }); const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); - const aggregations = - aggsPath.length > 0 ? get(respChartsData.aggregations, aggsPath) : respChartsData.aggregations; + const aggregations = aggsPath.length > 0 ? get(body.aggregations, aggsPath) : body.aggregations; const chartsData: ChartData[] = fields.map( (field): ChartData => { @@ -350,12 +348,12 @@ export const getHistogramsForFields = async ( }; export class DataVisualizer { - private _mlClusterClient: ILegacyScopedClusterClient; - private _callAsCurrentUser: ILegacyScopedClusterClient['callAsCurrentUser']; + private _client: IScopedClusterClient; + private _asCurrentUser: IScopedClusterClient['asCurrentUser']; - constructor(mlClusterClient: ILegacyScopedClusterClient) { - this._callAsCurrentUser = mlClusterClient.callAsCurrentUser; - this._mlClusterClient = mlClusterClient; + constructor(client: IScopedClusterClient) { + this._asCurrentUser = client.asCurrentUser; + this._client = client; } // Obtains overall stats on the fields in the supplied index pattern, returning an object @@ -451,7 +449,7 @@ export class DataVisualizer { samplerShardSize: number ): Promise { return await getHistogramsForFields( - this._mlClusterClient, + this._client, indexPatternTitle, query, fields, @@ -621,7 +619,7 @@ export class DataVisualizer { }; }); - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -630,14 +628,14 @@ export class DataVisualizer { aggs: buildSamplerAggregation(aggs, samplerShardSize), }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, rest_total_hits_as_int: true, size, - body, + body: searchBody, }); - const aggregations = resp.aggregations; - const totalCount = get(resp, ['hits', 'total'], 0); + const aggregations = body.aggregations; + const totalCount = get(body, ['hits', 'total'], 0); const stats = { totalCount, aggregatableExistsFields: [] as FieldData[], @@ -688,7 +686,7 @@ export class DataVisualizer { const size = 0; const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query); - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -697,13 +695,13 @@ export class DataVisualizer { }; filterCriteria.push({ exists: { field } }); - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, rest_total_hits_as_int: true, size, - body, + body: searchBody, }); - return resp.hits.total > 0; + return body.hits.total > 0; } async getDocumentCountStats( @@ -730,7 +728,7 @@ export class DataVisualizer { }, }; - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -739,15 +737,15 @@ export class DataVisualizer { aggs, }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, size, - body, + body: searchBody, }); const buckets: { [key: string]: number } = {}; const dataByTimeBucket: Array<{ key: string; doc_count: number }> = get( - resp, + body, ['aggregations', 'eventRate', 'buckets'], [] ); @@ -833,7 +831,7 @@ export class DataVisualizer { } }); - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -842,12 +840,12 @@ export class DataVisualizer { aggs: buildSamplerAggregation(aggs, samplerShardSize), }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, size, - body, + body: searchBody, }); - const aggregations = resp.aggregations; + const aggregations = body.aggregations; const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); const batchStats: NumericFieldStats[] = []; fields.forEach((field, i) => { @@ -954,7 +952,7 @@ export class DataVisualizer { } }); - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -963,12 +961,12 @@ export class DataVisualizer { aggs: buildSamplerAggregation(aggs, samplerShardSize), }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, size, - body, + body: searchBody, }); - const aggregations = resp.aggregations; + const aggregations = body.aggregations; const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); const batchStats: StringFieldStats[] = []; fields.forEach((field, i) => { @@ -1028,7 +1026,7 @@ export class DataVisualizer { }; }); - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -1037,12 +1035,12 @@ export class DataVisualizer { aggs: buildSamplerAggregation(aggs, samplerShardSize), }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, size, - body, + body: searchBody, }); - const aggregations = resp.aggregations; + const aggregations = body.aggregations; const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); const batchStats: DateFieldStats[] = []; fields.forEach((field, i) => { @@ -1095,7 +1093,7 @@ export class DataVisualizer { }; }); - const body = { + const searchBody = { query: { bool: { filter: filterCriteria, @@ -1104,12 +1102,12 @@ export class DataVisualizer { aggs: buildSamplerAggregation(aggs, samplerShardSize), }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, size, - body, + body: searchBody, }); - const aggregations = resp.aggregations; + const aggregations = body.aggregations; const aggsPath = getSamplerAggregationsResponsePath(samplerShardSize); const batchStats: BooleanFieldStats[] = []; fields.forEach((field, i) => { @@ -1157,7 +1155,7 @@ export class DataVisualizer { exists: { field }, }); - const body = { + const searchBody = { _source: field, query: { bool: { @@ -1166,18 +1164,18 @@ export class DataVisualizer { }, }; - const resp = await this._callAsCurrentUser('search', { + const { body } = await this._asCurrentUser.search({ index, rest_total_hits_as_int: true, size, - body, + body: searchBody, }); const stats = { fieldName: field, examples: [] as any[], }; - if (resp.hits.total !== 0) { - const hits = resp.hits.hits; + if (body.hits.total !== 0) { + const hits = body.hits.hits; for (let i = 0; i < hits.length; i++) { // Look in the _source for the field value. // If the field is not in the _source (as will happen if the diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index 43a6876f76c49..68427e98750eb 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { duration } from 'moment'; import { parseInterval } from '../../../common/util/parse_interval'; import { initCardinalityFieldsCache } from './fields_aggs_cache'; @@ -14,7 +14,7 @@ import { initCardinalityFieldsCache } from './fields_aggs_cache'; * Service for carrying out queries to obtain data * specific to fields in Elasticsearch indices. */ -export function fieldsServiceProvider({ callAsCurrentUser }: ILegacyScopedClusterClient) { +export function fieldsServiceProvider({ asCurrentUser }: IScopedClusterClient) { const fieldsAggsCache = initCardinalityFieldsCache(); /** @@ -37,13 +37,13 @@ export function fieldsServiceProvider({ callAsCurrentUser }: ILegacyScopedCluste index: string | string[], fieldNames: string[] ): Promise { - const fieldCapsResp = await callAsCurrentUser('fieldCaps', { + const { body } = await asCurrentUser.fieldCaps({ index, fields: fieldNames, }); const aggregatableFields: string[] = []; fieldNames.forEach((fieldName) => { - const fieldInfo = fieldCapsResp.fields[fieldName]; + const fieldInfo = body.fields[fieldName]; const typeKeys = fieldInfo !== undefined ? Object.keys(fieldInfo) : []; if (typeKeys.length > 0) { const fieldType = typeKeys[0]; @@ -130,12 +130,12 @@ export function fieldsServiceProvider({ callAsCurrentUser }: ILegacyScopedCluste aggs, }; - const aggregations = ( - await callAsCurrentUser('search', { - index, - body, - }) - )?.aggregations; + const { + body: { aggregations }, + } = await asCurrentUser.search({ + index, + body, + }); if (!aggregations) { return {}; @@ -170,7 +170,9 @@ export function fieldsServiceProvider({ callAsCurrentUser }: ILegacyScopedCluste }> { const obj = { success: true, start: { epoch: 0, string: '' }, end: { epoch: 0, string: '' } }; - const resp = await callAsCurrentUser('search', { + const { + body: { aggregations }, + } = await asCurrentUser.search({ index, size: 0, body: { @@ -190,12 +192,12 @@ export function fieldsServiceProvider({ callAsCurrentUser }: ILegacyScopedCluste }, }); - if (resp.aggregations && resp.aggregations.earliest && resp.aggregations.latest) { - obj.start.epoch = resp.aggregations.earliest.value; - obj.start.string = resp.aggregations.earliest.value_as_string; + if (aggregations && aggregations.earliest && aggregations.latest) { + obj.start.epoch = aggregations.earliest.value; + obj.start.string = aggregations.earliest.value_as_string; - obj.end.epoch = resp.aggregations.latest.value; - obj.end.string = resp.aggregations.latest.value_as_string; + obj.end.epoch = aggregations.latest.value; + obj.end.string = aggregations.latest.value_as_string; } return obj; } @@ -338,12 +340,12 @@ export function fieldsServiceProvider({ callAsCurrentUser }: ILegacyScopedCluste }, }; - const aggregations = ( - await callAsCurrentUser('search', { - index, - body, - }) - )?.aggregations; + const { + body: { aggregations }, + } = await asCurrentUser.search({ + index, + body, + }); if (!aggregations) { return cachedValues; diff --git a/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts index 9cd71c046b66c..9e502c04fbb7b 100644 --- a/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { AnalysisResult, FormattedOverrides, InputOverrides, + FindFileStructureResponse, } from '../../../common/types/file_datavisualizer'; export type InputData = any[]; -export function fileDataVisualizerProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { - async function analyzeFile(data: any, overrides: any): Promise { - const results = await callAsInternalUser('ml.fileStructure', { +export function fileDataVisualizerProvider({ asInternalUser }: IScopedClusterClient) { + async function analyzeFile(data: InputData, overrides: InputOverrides): Promise { + overrides.explain = overrides.explain === undefined ? 'true' : overrides.explain; + const { body } = await asInternalUser.ml.findFileStructure({ body: data, ...overrides, }); @@ -24,7 +26,7 @@ export function fileDataVisualizerProvider({ callAsInternalUser }: ILegacyScoped return { ...(hasOverrides && { overrides: reducedOverrides }), - results, + results: body, }; } diff --git a/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts index fc9b333298c9d..6108454c08aa7 100644 --- a/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts +++ b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_datavisualizer'; import { ImportResponse, @@ -15,7 +15,7 @@ import { } from '../../../common/types/file_datavisualizer'; import { InputData } from './file_data_visualizer'; -export function importDataProvider({ callAsCurrentUser }: ILegacyScopedClusterClient) { +export function importDataProvider({ asCurrentUser }: IScopedClusterClient) { async function importData( id: string, index: string, @@ -40,9 +40,9 @@ export function importDataProvider({ callAsCurrentUser }: ILegacyScopedClusterCl // create the pipeline if one has been supplied if (pipelineId !== undefined) { - const success = await createPipeline(pipelineId, pipeline); - if (success.acknowledged !== true) { - throw success; + const resp = await createPipeline(pipelineId, pipeline); + if (resp.acknowledged !== true) { + throw resp; } } createdPipelineId = pipelineId; @@ -80,7 +80,7 @@ export function importDataProvider({ callAsCurrentUser }: ILegacyScopedClusterCl id, index: createdIndex, pipelineId: createdPipelineId, - error: error.error !== undefined ? error.error : error, + error: error.body !== undefined ? error.body : error, docCount, ingestError: error.ingestError, failures: error.failures || [], @@ -102,7 +102,7 @@ export function importDataProvider({ callAsCurrentUser }: ILegacyScopedClusterCl body.settings = settings; } - await callAsCurrentUser('indices.create', { index, body }); + await asCurrentUser.indices.create({ index, body }); } async function indexData(index: string, pipelineId: string, data: InputData) { @@ -118,7 +118,7 @@ export function importDataProvider({ callAsCurrentUser }: ILegacyScopedClusterCl settings.pipeline = pipelineId; } - const resp = await callAsCurrentUser('bulk', settings); + const { body: resp } = await asCurrentUser.bulk(settings); if (resp.errors) { throw resp; } else { @@ -151,7 +151,8 @@ export function importDataProvider({ callAsCurrentUser }: ILegacyScopedClusterCl } async function createPipeline(id: string, pipeline: any) { - return await callAsCurrentUser('ingest.putPipeline', { id, body: pipeline }); + const { body } = await asCurrentUser.ingest.putPipeline({ id, body: pipeline }); + return body; } function getFailures(items: any[], data: InputData): ImportFailure[] { diff --git a/x-pack/plugins/ml/server/models/filter/filter_manager.ts b/x-pack/plugins/ml/server/models/filter/filter_manager.ts index 768ca1f893b68..19ba1b76f8a60 100644 --- a/x-pack/plugins/ml/server/models/filter/filter_manager.ts +++ b/x-pack/plugins/ml/server/models/filter/filter_manager.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { DetectorRule, DetectorRuleScope } from '../../../common/types/detector_rules'; @@ -58,26 +58,26 @@ interface PartialJob { } export class FilterManager { - private _callAsInternalUser: ILegacyScopedClusterClient['callAsInternalUser']; - constructor({ callAsInternalUser }: ILegacyScopedClusterClient) { - this._callAsInternalUser = callAsInternalUser; + private _asInternalUser: IScopedClusterClient['asInternalUser']; + constructor({ asInternalUser }: IScopedClusterClient) { + this._asInternalUser = asInternalUser; } async getFilter(filterId: string) { try { const [JOBS, FILTERS] = [0, 1]; const results = await Promise.all([ - this._callAsInternalUser('ml.jobs'), - this._callAsInternalUser('ml.filters', { filterId }), + this._asInternalUser.ml.getJobs(), + this._asInternalUser.ml.getFilters({ filter_id: filterId }), ]); - if (results[FILTERS] && results[FILTERS].filters.length) { + if (results[FILTERS] && results[FILTERS].body.filters.length) { let filtersInUse: FiltersInUse = {}; - if (results[JOBS] && results[JOBS].jobs) { - filtersInUse = this.buildFiltersInUse(results[JOBS].jobs); + if (results[JOBS] && results[JOBS].body.jobs) { + filtersInUse = this.buildFiltersInUse(results[JOBS].body.jobs); } - const filter = results[FILTERS].filters[0]; + const filter = results[FILTERS].body.filters[0]; filter.used_by = filtersInUse[filter.filter_id]; return filter; } else { @@ -90,8 +90,8 @@ export class FilterManager { async getAllFilters() { try { - const filtersResp = await this._callAsInternalUser('ml.filters'); - return filtersResp.filters; + const { body } = await this._asInternalUser.ml.getFilters({ size: 1000 }); + return body.filters; } catch (error) { throw Boom.badRequest(error); } @@ -101,14 +101,14 @@ export class FilterManager { try { const [JOBS, FILTERS] = [0, 1]; const results = await Promise.all([ - this._callAsInternalUser('ml.jobs'), - this._callAsInternalUser('ml.filters'), + this._asInternalUser.ml.getJobs(), + this._asInternalUser.ml.getFilters({ size: 1000 }), ]); // Build a map of filter_ids against jobs and detectors using that filter. let filtersInUse: FiltersInUse = {}; - if (results[JOBS] && results[JOBS].jobs) { - filtersInUse = this.buildFiltersInUse(results[JOBS].jobs); + if (results[JOBS] && results[JOBS].body.jobs) { + filtersInUse = this.buildFiltersInUse(results[JOBS].body.jobs); } // For each filter, return just @@ -117,8 +117,8 @@ export class FilterManager { // item_count // jobs using the filter const filterStats: FilterStats[] = []; - if (results[FILTERS] && results[FILTERS].filters) { - results[FILTERS].filters.forEach((filter: Filter) => { + if (results[FILTERS] && results[FILTERS].body.filters) { + results[FILTERS].body.filters.forEach((filter: Filter) => { const stats: FilterStats = { filter_id: filter.filter_id, description: filter.description, @@ -139,7 +139,8 @@ export class FilterManager { const { filterId, ...body } = filter; try { // Returns the newly created filter. - return await this._callAsInternalUser('ml.addFilter', { filterId, body }); + const { body: resp } = await this._asInternalUser.ml.putFilter({ filter_id: filterId, body }); + return resp; } catch (error) { throw Boom.badRequest(error); } @@ -159,17 +160,19 @@ export class FilterManager { } // Returns the newly updated filter. - return await this._callAsInternalUser('ml.updateFilter', { - filterId, + const { body: resp } = await this._asInternalUser.ml.updateFilter({ + filter_id: filterId, body, }); + return resp; } catch (error) { throw Boom.badRequest(error); } } async deleteFilter(filterId: string) { - return this._callAsInternalUser('ml.deleteFilter', { filterId }); + const { body } = await this._asInternalUser.ml.deleteFilter({ filter_id: filterId }); + return body; } buildFiltersInUse(jobsList: PartialJob[]) { diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts index d72552b548b82..afdd3e9bb8ce9 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.d.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; export function jobAuditMessagesProvider( - mlClusterClient: ILegacyScopedClusterClient + client: IScopedClusterClient ): { getJobAuditMessages: (jobId?: string, from?: string) => any; getAuditMessagesSummary: (jobIds?: string[]) => any; diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js index 86d80c394137f..3fd5ebf3f68f4 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js @@ -34,14 +34,14 @@ const anomalyDetectorTypeFilter = { }, }; -export function jobAuditMessagesProvider({ callAsInternalUser }) { +export function jobAuditMessagesProvider({ asInternalUser }) { // search for audit messages, // jobId is optional. without it, all jobs will be listed. // from is optional and should be a string formatted in ES time units. e.g. 12h, 1d, 7d async function getJobAuditMessages(jobId, from) { let gte = null; if (jobId !== undefined && from === undefined) { - const jobs = await callAsInternalUser('ml.jobs', { jobId }); + const jobs = await asInternalUser.ml.getJobs({ job_id: jobId }); if (jobs.count > 0 && jobs.jobs !== undefined) { gte = moment(jobs.jobs[0].create_time).valueOf(); } @@ -99,26 +99,22 @@ export function jobAuditMessagesProvider({ callAsInternalUser }) { }); } - try { - const resp = await callAsInternalUser('search', { - index: ML_NOTIFICATION_INDEX_PATTERN, - ignore_unavailable: true, - rest_total_hits_as_int: true, - size: SIZE, - body: { - sort: [{ timestamp: { order: 'desc' } }, { job_id: { order: 'asc' } }], - query, - }, - }); + const { body } = await asInternalUser.search({ + index: ML_NOTIFICATION_INDEX_PATTERN, + ignore_unavailable: true, + rest_total_hits_as_int: true, + size: SIZE, + body: { + sort: [{ timestamp: { order: 'desc' } }, { job_id: { order: 'asc' } }], + query, + }, + }); - let messages = []; - if (resp.hits.total !== 0) { - messages = resp.hits.hits.map((hit) => hit._source); - } - return messages; - } catch (e) { - throw e; + let messages = []; + if (body.hits.total !== 0) { + messages = body.hits.hits.map((hit) => hit._source); } + return messages; } // search highest, most recent audit messages for all jobs for the last 24hrs. @@ -128,65 +124,63 @@ export function jobAuditMessagesProvider({ callAsInternalUser }) { const maxBuckets = 10000; let levelsPerJobAggSize = maxBuckets; - try { - const query = { - bool: { - filter: [ - { - range: { - timestamp: { - gte: 'now-1d', - }, + const query = { + bool: { + filter: [ + { + range: { + timestamp: { + gte: 'now-1d', }, }, - anomalyDetectorTypeFilter, - ], - }, - }; - - // If the jobIds arg is supplied, add a query filter - // to only include those jobIds in the aggregations. - if (Array.isArray(jobIds) && jobIds.length > 0) { - query.bool.filter.push({ - terms: { - job_id: jobIds, }, - }); - levelsPerJobAggSize = jobIds.length; - } + anomalyDetectorTypeFilter, + ], + }, + }; - const resp = await callAsInternalUser('search', { - index: ML_NOTIFICATION_INDEX_PATTERN, - ignore_unavailable: true, - rest_total_hits_as_int: true, - size: 0, - body: { - query, - aggs: { - levelsPerJob: { - terms: { - field: 'job_id', - size: levelsPerJobAggSize, - }, - aggs: { - levels: { - terms: { - field: 'level', - }, - aggs: { - latestMessage: { - terms: { - field: 'message.raw', - size: 1, - order: { - latestMessage: 'desc', - }, + // If the jobIds arg is supplied, add a query filter + // to only include those jobIds in the aggregations. + if (Array.isArray(jobIds) && jobIds.length > 0) { + query.bool.filter.push({ + terms: { + job_id: jobIds, + }, + }); + levelsPerJobAggSize = jobIds.length; + } + + const { body } = await asInternalUser.search({ + index: ML_NOTIFICATION_INDEX_PATTERN, + ignore_unavailable: true, + rest_total_hits_as_int: true, + size: 0, + body: { + query, + aggs: { + levelsPerJob: { + terms: { + field: 'job_id', + size: levelsPerJobAggSize, + }, + aggs: { + levels: { + terms: { + field: 'level', + }, + aggs: { + latestMessage: { + terms: { + field: 'message.raw', + size: 1, + order: { + latestMessage: 'desc', }, - aggs: { - latestMessage: { - max: { - field: 'timestamp', - }, + }, + aggs: { + latestMessage: { + max: { + field: 'timestamp', }, }, }, @@ -196,67 +190,65 @@ export function jobAuditMessagesProvider({ callAsInternalUser }) { }, }, }, - }); + }, + }); - let messagesPerJob = []; - const jobMessages = []; - if ( - resp.hits.total !== 0 && - resp.aggregations && - resp.aggregations.levelsPerJob && - resp.aggregations.levelsPerJob.buckets && - resp.aggregations.levelsPerJob.buckets.length - ) { - messagesPerJob = resp.aggregations.levelsPerJob.buckets; - } + let messagesPerJob = []; + const jobMessages = []; + if ( + body.hits.total !== 0 && + body.aggregations && + body.aggregations.levelsPerJob && + body.aggregations.levelsPerJob.buckets && + body.aggregations.levelsPerJob.buckets.length + ) { + messagesPerJob = body.aggregations.levelsPerJob.buckets; + } - messagesPerJob.forEach((job) => { - // ignore system messages (id==='') - if (job.key !== '' && job.levels && job.levels.buckets && job.levels.buckets.length) { - let highestLevel = 0; - let highestLevelText = ''; - let msgTime = 0; + messagesPerJob.forEach((job) => { + // ignore system messages (id==='') + if (job.key !== '' && job.levels && job.levels.buckets && job.levels.buckets.length) { + let highestLevel = 0; + let highestLevelText = ''; + let msgTime = 0; - job.levels.buckets.forEach((level) => { - const label = level.key; - // note the highest message level - if (LEVEL[label] > highestLevel) { - highestLevel = LEVEL[label]; - if ( - level.latestMessage && - level.latestMessage.buckets && - level.latestMessage.buckets.length - ) { - level.latestMessage.buckets.forEach((msg) => { - // there should only be one result here. - highestLevelText = msg.key; + job.levels.buckets.forEach((level) => { + const label = level.key; + // note the highest message level + if (LEVEL[label] > highestLevel) { + highestLevel = LEVEL[label]; + if ( + level.latestMessage && + level.latestMessage.buckets && + level.latestMessage.buckets.length + ) { + level.latestMessage.buckets.forEach((msg) => { + // there should only be one result here. + highestLevelText = msg.key; - // note the time in ms for the highest level - // so we can filter them out later if they're earlier than the - // job's create time. - if (msg.latestMessage && msg.latestMessage.value_as_string) { - const time = moment(msg.latestMessage.value_as_string); - msgTime = time.valueOf(); - } - }); - } + // note the time in ms for the highest level + // so we can filter them out later if they're earlier than the + // job's create time. + if (msg.latestMessage && msg.latestMessage.value_as_string) { + const time = moment(msg.latestMessage.value_as_string); + msgTime = time.valueOf(); + } + }); } - }); - - if (msgTime !== 0 && highestLevel !== 0) { - jobMessages.push({ - job_id: job.key, - highestLevelText, - highestLevel: levelToText(highestLevel), - msgTime, - }); } + }); + + if (msgTime !== 0 && highestLevel !== 0) { + jobMessages.push({ + job_id: job.key, + highestLevelText, + highestLevel: levelToText(highestLevel), + msgTime, + }); } - }); - return jobMessages; - } catch (e) { - throw e; - } + } + }); + return jobMessages; } function levelToText(level) { diff --git a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts index 98e1be48bb766..c0eb1b72825df 100644 --- a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts +++ b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; @@ -26,7 +26,7 @@ interface Results { }; } -export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { +export function datafeedsProvider({ asInternalUser }: IScopedClusterClient) { async function forceStartDatafeeds(datafeedIds: string[], start?: number, end?: number) { const jobIds = await getJobIdsByDatafeedId(); const doStartsCalled = datafeedIds.reduce((acc, cur) => { @@ -42,8 +42,8 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl try { await startDatafeed(datafeedId, start, end); return { started: true }; - } catch (error) { - return { started: false, error }; + } catch ({ body }) { + return { started: false, error: body }; } } else { return { started: true }; @@ -66,7 +66,7 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl results[datafeedId] = await doStart(datafeedId); return fillResultsWithTimeouts(results, datafeedId, datafeedIds, JOB_STATE.OPENED); } - results[datafeedId] = { started: false, error }; + results[datafeedId] = { started: false, error: error.body }; } } else { results[datafeedId] = { @@ -84,8 +84,8 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl async function openJob(jobId: string) { let opened = false; try { - const resp = await callAsInternalUser('ml.openJob', { jobId }); - opened = resp.opened; + const { body } = await asInternalUser.ml.openJob({ job_id: jobId }); + opened = body.opened; } catch (error) { if (error.statusCode === 409) { opened = true; @@ -97,7 +97,11 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl } async function startDatafeed(datafeedId: string, start?: number, end?: number) { - return callAsInternalUser('ml.startDatafeed', { datafeedId, start, end }); + return asInternalUser.ml.startDatafeed({ + datafeed_id: datafeedId, + start: (start as unknown) as string, + end: (end as unknown) as string, + }); } async function stopDatafeeds(datafeedIds: string[]) { @@ -105,7 +109,12 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl for (const datafeedId of datafeedIds) { try { - results[datafeedId] = await callAsInternalUser('ml.stopDatafeed', { datafeedId }); + const { body } = await asInternalUser.ml.stopDatafeed<{ + started: boolean; + }>({ + datafeed_id: datafeedId, + }); + results[datafeedId] = body; } catch (error) { if (isRequestTimeout(error)) { return fillResultsWithTimeouts(results, datafeedId, datafeedIds, DATAFEED_STATE.STOPPED); @@ -117,11 +126,17 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl } async function forceDeleteDatafeed(datafeedId: string) { - return callAsInternalUser('ml.deleteDatafeed', { datafeedId, force: true }); + const { body } = await asInternalUser.ml.deleteDatafeed({ + datafeed_id: datafeedId, + force: true, + }); + return body; } async function getDatafeedIdsByJobId() { - const { datafeeds } = (await callAsInternalUser('ml.datafeeds')) as MlDatafeedsResponse; + const { + body: { datafeeds }, + } = await asInternalUser.ml.getDatafeeds(); return datafeeds.reduce((acc, cur) => { acc[cur.job_id] = cur.datafeed_id; return acc; @@ -129,7 +144,9 @@ export function datafeedsProvider({ callAsInternalUser }: ILegacyScopedClusterCl } async function getJobIdsByDatafeedId() { - const { datafeeds } = (await callAsInternalUser('ml.datafeeds')) as MlDatafeedsResponse; + const { + body: { datafeeds }, + } = await asInternalUser.ml.getDatafeeds(); return datafeeds.reduce((acc, cur) => { acc[cur.datafeed_id] = cur.job_id; return acc; diff --git a/x-pack/plugins/ml/server/models/job_service/error_utils.ts b/x-pack/plugins/ml/server/models/job_service/error_utils.ts index 8a47993546fb8..dc871a9dce805 100644 --- a/x-pack/plugins/ml/server/models/job_service/error_utils.ts +++ b/x-pack/plugins/ml/server/models/job_service/error_utils.ts @@ -7,11 +7,11 @@ import { i18n } from '@kbn/i18n'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; -const REQUEST_TIMEOUT = 'RequestTimeout'; +const REQUEST_TIMEOUT_NAME = 'RequestTimeout'; type ACTION_STATE = DATAFEED_STATE | JOB_STATE; -export function isRequestTimeout(error: { displayName: string }) { - return error.displayName === REQUEST_TIMEOUT; +export function isRequestTimeout(error: { name: string }) { + return error.name === REQUEST_TIMEOUT_NAME; } interface Results { diff --git a/x-pack/plugins/ml/server/models/job_service/groups.ts b/x-pack/plugins/ml/server/models/job_service/groups.ts index c4ea854c14f87..0f53d27f2eddf 100644 --- a/x-pack/plugins/ml/server/models/job_service/groups.ts +++ b/x-pack/plugins/ml/server/models/job_service/groups.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { CalendarManager } from '../calendar'; import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; import { Job } from '../../../common/types/anomaly_detection_jobs'; @@ -23,18 +23,19 @@ interface Results { }; } -export function groupsProvider(mlClusterClient: ILegacyScopedClusterClient) { - const calMngr = new CalendarManager(mlClusterClient); - const { callAsInternalUser } = mlClusterClient; +export function groupsProvider(client: IScopedClusterClient) { + const calMngr = new CalendarManager(client); + const { asInternalUser } = client; async function getAllGroups() { const groups: { [id: string]: Group } = {}; const jobIds: { [id: string]: undefined | null } = {}; - const [{ jobs }, calendars] = await Promise.all([ - callAsInternalUser('ml.jobs') as Promise, + const [{ body }, calendars] = await Promise.all([ + asInternalUser.ml.getJobs(), calMngr.getAllCalendars(), ]); + const { jobs } = body; if (jobs) { jobs.forEach((job) => { jobIds[job.job_id] = null; @@ -80,10 +81,10 @@ export function groupsProvider(mlClusterClient: ILegacyScopedClusterClient) { for (const job of jobs) { const { job_id: jobId, groups } = job; try { - await callAsInternalUser('ml.updateJob', { jobId, body: { groups } }); + await asInternalUser.ml.updateJob({ job_id: jobId, body: { groups } }); results[jobId] = { success: true }; - } catch (error) { - results[jobId] = { success: false, error }; + } catch ({ body }) { + results[jobId] = { success: false, error: body }; } } return results; diff --git a/x-pack/plugins/ml/server/models/job_service/index.ts b/x-pack/plugins/ml/server/models/job_service/index.ts index 1ff33a7b00f0b..6fea5d3b5a491 100644 --- a/x-pack/plugins/ml/server/models/job_service/index.ts +++ b/x-pack/plugins/ml/server/models/job_service/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { datafeedsProvider } from './datafeeds'; import { jobsProvider } from './jobs'; import { groupsProvider } from './groups'; @@ -12,14 +12,14 @@ import { newJobCapsProvider } from './new_job_caps'; import { newJobChartsProvider, topCategoriesProvider } from './new_job'; import { modelSnapshotProvider } from './model_snapshots'; -export function jobServiceProvider(mlClusterClient: ILegacyScopedClusterClient) { +export function jobServiceProvider(client: IScopedClusterClient) { return { - ...datafeedsProvider(mlClusterClient), - ...jobsProvider(mlClusterClient), - ...groupsProvider(mlClusterClient), - ...newJobCapsProvider(mlClusterClient), - ...newJobChartsProvider(mlClusterClient), - ...topCategoriesProvider(mlClusterClient), - ...modelSnapshotProvider(mlClusterClient), + ...datafeedsProvider(client), + ...jobsProvider(client), + ...groupsProvider(client), + ...newJobCapsProvider(client), + ...newJobChartsProvider(client), + ...topCategoriesProvider(client), + ...modelSnapshotProvider(client), }; } diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 0aa1cfdae13c7..e047d31ba6eb7 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { uniq } from 'lodash'; import Boom from 'boom'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { parseTimeIntervalForJob } from '../../../common/util/job_utils'; import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; import { @@ -22,7 +22,7 @@ import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; import { datafeedsProvider, MlDatafeedsResponse, MlDatafeedsStatsResponse } from './datafeeds'; import { jobAuditMessagesProvider } from '../job_audit_messages'; import { resultsServiceProvider } from '../results_service'; -import { CalendarManager, Calendar } from '../calendar'; +import { CalendarManager } from '../calendar'; import { fillResultsWithTimeouts, isRequestTimeout } from './error_utils'; import { getEarliestDatafeedStartTime, @@ -47,16 +47,16 @@ interface Results { }; } -export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { - const { callAsInternalUser } = mlClusterClient; +export function jobsProvider(client: IScopedClusterClient) { + const { asInternalUser } = client; - const { forceDeleteDatafeed, getDatafeedIdsByJobId } = datafeedsProvider(mlClusterClient); - const { getAuditMessagesSummary } = jobAuditMessagesProvider(mlClusterClient); - const { getLatestBucketTimestampByJob } = resultsServiceProvider(mlClusterClient); - const calMngr = new CalendarManager(mlClusterClient); + const { forceDeleteDatafeed, getDatafeedIdsByJobId } = datafeedsProvider(client); + const { getAuditMessagesSummary } = jobAuditMessagesProvider(client); + const { getLatestBucketTimestampByJob } = resultsServiceProvider(client); + const calMngr = new CalendarManager(client); async function forceDeleteJob(jobId: string) { - return callAsInternalUser('ml.deleteJob', { jobId, force: true }); + return asInternalUser.ml.deleteJob({ job_id: jobId, force: true, wait_for_completion: false }); } async function deleteJobs(jobIds: string[]) { @@ -78,7 +78,7 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { if (isRequestTimeout(error)) { return fillResultsWithTimeouts(results, jobId, jobIds, DATAFEED_STATE.DELETED); } - results[jobId] = { deleted: false, error }; + results[jobId] = { deleted: false, error: error.body }; } } } catch (error) { @@ -90,7 +90,7 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { DATAFEED_STATE.DELETED ); } - results[jobId] = { deleted: false, error }; + results[jobId] = { deleted: false, error: error.body }; } } return results; @@ -100,7 +100,7 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { const results: Results = {}; for (const jobId of jobIds) { try { - await callAsInternalUser('ml.closeJob', { jobId }); + await asInternalUser.ml.closeJob({ job_id: jobId }); results[jobId] = { closed: true }; } catch (error) { if (isRequestTimeout(error)) { @@ -109,23 +109,23 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { if ( error.statusCode === 409 && - error.response && - error.response.includes('datafeed') === false + error.body.error?.reason && + error.body.error.reason.includes('datafeed') === false ) { // the close job request may fail (409) if the job has failed or if the datafeed hasn't been stopped. // if the job has failed we want to attempt a force close. // however, if we received a 409 due to the datafeed being started we should not attempt a force close. try { - await callAsInternalUser('ml.closeJob', { jobId, force: true }); + await asInternalUser.ml.closeJob({ job_id: jobId, force: true }); results[jobId] = { closed: true }; } catch (error2) { - if (isRequestTimeout(error)) { + if (isRequestTimeout(error2)) { return fillResultsWithTimeouts(results, jobId, jobIds, JOB_STATE.CLOSED); } - results[jobId] = { closed: false, error: error2 }; + results[jobId] = { closed: false, error: error2.body }; } } else { - results[jobId] = { closed: false, error }; + results[jobId] = { closed: false, error: error.body }; } } } @@ -139,12 +139,12 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { throw Boom.notFound(`Cannot find datafeed for job ${jobId}`); } - const dfResult = await callAsInternalUser('ml.stopDatafeed', { datafeedId, force: true }); - if (!dfResult || dfResult.stopped !== true) { + const { body } = await asInternalUser.ml.stopDatafeed({ datafeed_id: datafeedId, force: true }); + if (body.stopped !== true) { return { success: false }; } - await callAsInternalUser('ml.closeJob', { jobId, force: true }); + await asInternalUser.ml.closeJob({ job_id: jobId, force: true }); return { success: true }; } @@ -256,41 +256,26 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { const calendarsByJobId: { [jobId: string]: string[] } = {}; const globalCalendars: string[] = []; - const requests: [ - Promise, - Promise, - Promise, - Promise, - Promise, - Promise<{ [id: string]: number | undefined }> - ] = [ - jobIds.length > 0 - ? (callAsInternalUser('ml.jobs', { jobId: jobIds }) as Promise) // move length check in side call - : (callAsInternalUser('ml.jobs') as Promise), - jobIds.length > 0 - ? (callAsInternalUser('ml.jobStats', { jobId: jobIds }) as Promise) - : (callAsInternalUser('ml.jobStats') as Promise), - callAsInternalUser('ml.datafeeds') as Promise, - callAsInternalUser('ml.datafeedStats') as Promise, - calMngr.getAllCalendars(), - getLatestBucketTimestampByJob(), - ]; - + const jobIdsString = jobIds.join(); const [ - jobResults, - jobStatsResults, - datafeedResults, - datafeedStatsResults, + { body: jobResults }, + { body: jobStatsResults }, + { body: datafeedResults }, + { body: datafeedStatsResults }, calendarResults, latestBucketTimestampByJob, - ] = await Promise.all< - MlJobsResponse, - MlJobsStatsResponse, - MlDatafeedsResponse, - MlDatafeedsStatsResponse, - Calendar[], - { [id: string]: number | undefined } - >(requests); + ] = await Promise.all([ + asInternalUser.ml.getJobs( + jobIds.length > 0 ? { job_id: jobIdsString } : undefined + ), + asInternalUser.ml.getJobStats( + jobIds.length > 0 ? { job_id: jobIdsString } : undefined + ), + asInternalUser.ml.getDatafeeds(), + asInternalUser.ml.getDatafeedStats(), + calMngr.getAllCalendars(), + getLatestBucketTimestampByJob(), + ]); if (datafeedResults && datafeedResults.datafeeds) { datafeedResults.datafeeds.forEach((datafeed) => { @@ -400,9 +385,9 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { const detailed = true; const jobIds = []; try { - const tasksList = await callAsInternalUser('tasks.list', { actions, detailed }); - Object.keys(tasksList.nodes).forEach((nodeId) => { - const tasks = tasksList.nodes[nodeId].tasks; + const { body } = await asInternalUser.tasks.list({ actions, detailed }); + Object.keys(body.nodes).forEach((nodeId) => { + const tasks = body.nodes[nodeId].tasks; Object.keys(tasks).forEach((taskId) => { jobIds.push(tasks[taskId].description.replace(/^delete-job-/, '')); }); @@ -410,7 +395,9 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { } catch (e) { // if the user doesn't have permission to load the task list, // use the jobs list to get the ids of deleting jobs - const { jobs } = (await callAsInternalUser('ml.jobs')) as MlJobsResponse; + const { + body: { jobs }, + } = await asInternalUser.ml.getJobs(); jobIds.push(...jobs.filter((j) => j.deleting === true).map((j) => j.job_id)); } return { jobIds }; @@ -421,13 +408,13 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { // e.g. *_low_request_rate_ecs async function jobsExist(jobIds: string[] = []) { // Get the list of job IDs. - const jobsInfo = (await callAsInternalUser('ml.jobs', { - jobId: jobIds, - })) as MlJobsResponse; + const { body } = await asInternalUser.ml.getJobs({ + job_id: jobIds.join(), + }); const results: { [id: string]: boolean } = {}; - if (jobsInfo.count > 0) { - const allJobIds = jobsInfo.jobs.map((job) => job.job_id); + if (body.count > 0) { + const allJobIds = body.jobs.map((job) => job.job_id); // Check if each of the supplied IDs match existing jobs. jobIds.forEach((jobId) => { @@ -446,9 +433,9 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { } async function getAllJobAndGroupIds() { - const { getAllGroups } = groupsProvider(mlClusterClient); - const jobs = (await callAsInternalUser('ml.jobs')) as MlJobsResponse; - const jobIds = jobs.jobs.map((job) => job.job_id); + const { getAllGroups } = groupsProvider(client); + const { body } = await asInternalUser.ml.getJobs(); + const jobIds = body.jobs.map((job) => job.job_id); const groups = await getAllGroups(); const groupIds = groups.map((group) => group.id); @@ -460,13 +447,13 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { async function getLookBackProgress(jobId: string, start: number, end: number) { const datafeedId = `datafeed-${jobId}`; - const [jobStats, isRunning] = await Promise.all([ - callAsInternalUser('ml.jobStats', { jobId: [jobId] }) as Promise, + const [{ body }, isRunning] = await Promise.all([ + asInternalUser.ml.getJobStats({ job_id: jobId }), isDatafeedRunning(datafeedId), ]); - if (jobStats.jobs.length) { - const statsForJob = jobStats.jobs[0]; + if (body.jobs.length) { + const statsForJob = body.jobs[0]; const time = statsForJob.data_counts.latest_record_timestamp; const progress = (time - start) / (end - start); const isJobClosed = statsForJob.state === JOB_STATE.CLOSED; @@ -480,11 +467,11 @@ export function jobsProvider(mlClusterClient: ILegacyScopedClusterClient) { } async function isDatafeedRunning(datafeedId: string) { - const stats = (await callAsInternalUser('ml.datafeedStats', { - datafeedId: [datafeedId], - })) as MlDatafeedsStatsResponse; - if (stats.datafeeds.length) { - const state = stats.datafeeds[0].state; + const { body } = await asInternalUser.ml.getDatafeedStats({ + datafeed_id: datafeedId, + }); + if (body.datafeeds.length) { + const state = body.datafeeds[0].state; return ( state === DATAFEED_STATE.STARTED || state === DATAFEED_STATE.STARTING || diff --git a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts index 576d6f8cbb160..34206a68ffeb9 100644 --- a/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts +++ b/x-pack/plugins/ml/server/models/job_service/model_snapshots.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ModelSnapshot } from '../../../common/types/anomaly_detection_jobs'; import { datafeedsProvider } from './datafeeds'; import { FormCalendar, CalendarManager } from '../calendar'; @@ -19,9 +19,9 @@ export interface RevertModelSnapshotResponse { model: ModelSnapshot; } -export function modelSnapshotProvider(mlClusterClient: ILegacyScopedClusterClient) { - const { callAsInternalUser } = mlClusterClient; - const { forceStartDatafeeds, getDatafeedIdsByJobId } = datafeedsProvider(mlClusterClient); +export function modelSnapshotProvider(client: IScopedClusterClient) { + const { asInternalUser } = client; + const { forceStartDatafeeds, getDatafeedIdsByJobId } = datafeedsProvider(client); async function revertModelSnapshot( jobId: string, @@ -33,13 +33,13 @@ export function modelSnapshotProvider(mlClusterClient: ILegacyScopedClusterClien ) { let datafeedId = `datafeed-${jobId}`; // ensure job exists - await callAsInternalUser('ml.jobs', { jobId: [jobId] }); + await asInternalUser.ml.getJobs({ job_id: jobId }); try { // ensure the datafeed exists // the datafeed is probably called datafeed- - await callAsInternalUser('ml.datafeeds', { - datafeedId: [datafeedId], + await asInternalUser.ml.getDatafeeds({ + datafeed_id: datafeedId, }); } catch (e) { // if the datafeed isn't called datafeed- @@ -52,19 +52,21 @@ export function modelSnapshotProvider(mlClusterClient: ILegacyScopedClusterClien } // ensure the snapshot exists - const snapshot = (await callAsInternalUser('ml.modelSnapshots', { - jobId, - snapshotId, - })) as ModelSnapshotsResponse; + const { body: snapshot } = await asInternalUser.ml.getModelSnapshots({ + job_id: jobId, + snapshot_id: snapshotId, + }); // apply the snapshot revert - const { model } = (await callAsInternalUser('ml.revertModelSnapshot', { - jobId, - snapshotId, + const { + body: { model }, + } = await asInternalUser.ml.revertModelSnapshot({ + job_id: jobId, + snapshot_id: snapshotId, body: { delete_intervening_results: deleteInterveningResults, }, - })) as RevertModelSnapshotResponse; + }); // create calendar (if specified) and replay datafeed if (replay && model.snapshot_id === snapshotId && snapshot.model_snapshots.length) { @@ -85,7 +87,7 @@ export function modelSnapshotProvider(mlClusterClient: ILegacyScopedClusterClien end_time: s.end, })), }; - const cm = new CalendarManager(mlClusterClient); + const cm = new CalendarManager(client); await cm.newCalendar(calendar); } diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts index ca3e0cef21049..6b9f30b2ae00b 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/examples.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { chunk } from 'lodash'; import { SearchResponse } from 'elasticsearch'; import { CATEGORY_EXAMPLES_SAMPLE_SIZE } from '../../../../../common/constants/categorization_job'; @@ -18,9 +18,9 @@ import { ValidationResults } from './validation_results'; const CHUNK_SIZE = 100; export function categorizationExamplesProvider({ - callAsCurrentUser, - callAsInternalUser, -}: ILegacyScopedClusterClient) { + asCurrentUser, + asInternalUser, +}: IScopedClusterClient) { const validationResults = new ValidationResults(); async function categorizationExamples( @@ -57,7 +57,7 @@ export function categorizationExamplesProvider({ } } - const results: SearchResponse<{ [id: string]: string }> = await callAsCurrentUser('search', { + const { body } = await asCurrentUser.search>({ index: indexPatternTitle, size, body: { @@ -67,7 +67,7 @@ export function categorizationExamplesProvider({ }, }); - const tempExamples = results.hits.hits.map(({ _source }) => _source[categorizationFieldName]); + const tempExamples = body.hits.hits.map(({ _source }) => _source[categorizationFieldName]); validationResults.createNullValueResult(tempExamples); @@ -112,7 +112,9 @@ export function categorizationExamplesProvider({ } async function loadTokens(examples: string[], analyzer: CategorizationAnalyzer) { - const { tokens }: { tokens: Token[] } = await callAsInternalUser('indices.analyze', { + const { + body: { tokens }, + } = await asInternalUser.indices.analyze<{ tokens: Token[] }>({ body: { ...getAnalyzer(analyzer), text: examples, diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts index 5ade86806f383..347afec8ef73c 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/top_categories.ts @@ -5,13 +5,13 @@ */ import { SearchResponse } from 'elasticsearch'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns'; import { CategoryId, Category } from '../../../../../common/types/categories'; -export function topCategoriesProvider({ callAsInternalUser }: ILegacyScopedClusterClient) { - async function getTotalCategories(jobId: string): Promise<{ total: number }> { - const totalResp = await callAsInternalUser('search', { +export function topCategoriesProvider({ asInternalUser }: IScopedClusterClient) { + async function getTotalCategories(jobId: string): Promise { + const { body } = await asInternalUser.search>({ index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -33,11 +33,12 @@ export function topCategoriesProvider({ callAsInternalUser }: ILegacyScopedClust }, }, }); - return totalResp?.hits?.total?.value ?? 0; + // @ts-ignore total is an object here + return body?.hits?.total?.value ?? 0; } async function getTopCategoryCounts(jobId: string, numberOfCategories: number) { - const top: SearchResponse = await callAsInternalUser('search', { + const { body } = await asInternalUser.search>({ index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -76,7 +77,7 @@ export function topCategoriesProvider({ callAsInternalUser }: ILegacyScopedClust const catCounts: Array<{ id: CategoryId; count: number; - }> = top.aggregations?.cat_count?.buckets.map((c: any) => ({ + }> = body.aggregations?.cat_count?.buckets.map((c: any) => ({ id: c.key, count: c.doc_count, })); @@ -99,7 +100,7 @@ export function topCategoriesProvider({ callAsInternalUser }: ILegacyScopedClust field: 'category_id', }, }; - const result: SearchResponse = await callAsInternalUser('search', { + const { body } = await asInternalUser.search>({ index: ML_RESULTS_INDEX_PATTERN, size, body: { @@ -118,7 +119,7 @@ export function topCategoriesProvider({ callAsInternalUser }: ILegacyScopedClust }, }); - return result.hits.hits?.map((c: { _source: Category }) => c._source) || []; + return body.hits.hits?.map((c: { _source: Category }) => c._source) || []; } async function topCategories(jobId: string, numberOfCategories: number) { diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts index 4b90283a3a966..60595ccedff45 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/categorization/validation_results.ts @@ -144,7 +144,7 @@ export class ValidationResults { this.createPrivilegesErrorResult(error); return; } - const message: string = error.message; + const message: string = error.body.error?.reason; if (message) { const rxp = /exceeded the allowed maximum of \[(\d+?)\]/; const match = rxp.exec(message); @@ -170,7 +170,7 @@ export class ValidationResults { } public createPrivilegesErrorResult(error: any) { - const message: string = error.message; + const message: string = error.body.error?.reason; if (message) { this._results.push({ id: VALIDATION_RESULT.INSUFFICIENT_PRIVILEGES, diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/charts.ts b/x-pack/plugins/ml/server/models/job_service/new_job/charts.ts index 63ae2c624ac38..da7d8d0577e4e 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/charts.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/charts.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { newJobLineChartProvider } from './line_chart'; import { newJobPopulationChartProvider } from './population_chart'; -export function newJobChartsProvider(mlClusterClient: ILegacyScopedClusterClient) { - const { newJobLineChart } = newJobLineChartProvider(mlClusterClient); - const { newJobPopulationChart } = newJobPopulationChartProvider(mlClusterClient); +export function newJobChartsProvider(client: IScopedClusterClient) { + const { newJobLineChart } = newJobLineChartProvider(client); + const { newJobPopulationChart } = newJobPopulationChartProvider(client); return { newJobLineChart, diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts index 3080b37867de5..9eea1ea2a28ae 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { AggFieldNamePair, EVENT_RATE_FIELD_ID } from '../../../../common/types/fields'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; @@ -23,7 +23,7 @@ interface ProcessedResults { totalResults: number; } -export function newJobLineChartProvider({ callAsCurrentUser }: ILegacyScopedClusterClient) { +export function newJobLineChartProvider({ asCurrentUser }: IScopedClusterClient) { async function newJobLineChart( indexPatternTitle: string, timeField: string, @@ -47,9 +47,9 @@ export function newJobLineChartProvider({ callAsCurrentUser }: ILegacyScopedClus splitFieldValue ); - const results = await callAsCurrentUser('search', json); + const { body } = await asCurrentUser.search(json); return processSearchResults( - results, + body, aggFieldNamePairs.map((af) => af.field) ); } diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts index ab75787a0069f..567afec809405 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { AggFieldNamePair, EVENT_RATE_FIELD_ID } from '../../../../common/types/fields'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; @@ -29,7 +29,7 @@ interface ProcessedResults { totalResults: number; } -export function newJobPopulationChartProvider({ callAsCurrentUser }: ILegacyScopedClusterClient) { +export function newJobPopulationChartProvider({ asCurrentUser }: IScopedClusterClient) { async function newJobPopulationChart( indexPatternTitle: string, timeField: string, @@ -51,15 +51,11 @@ export function newJobPopulationChartProvider({ callAsCurrentUser }: ILegacyScop splitFieldName ); - try { - const results = await callAsCurrentUser('search', json); - return processSearchResults( - results, - aggFieldNamePairs.map((af) => af.field) - ); - } catch (error) { - return { error }; - } + const { body } = await asCurrentUser.search(json); + return processSearchResults( + body, + aggFieldNamePairs.map((af) => af.field) + ); } return { diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index fd20610450cc1..c3b1de64c3eb5 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { cloneDeep } from 'lodash'; import { SavedObjectsClientContract } from 'kibana/server'; import { @@ -40,35 +40,36 @@ const supportedTypes: string[] = [ export function fieldServiceProvider( indexPattern: string, isRollup: boolean, - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract ) { - return new FieldsService(indexPattern, isRollup, mlClusterClient, savedObjectsClient); + return new FieldsService(indexPattern, isRollup, client, savedObjectsClient); } class FieldsService { private _indexPattern: string; private _isRollup: boolean; - private _mlClusterClient: ILegacyScopedClusterClient; + private _mlClusterClient: IScopedClusterClient; private _savedObjectsClient: SavedObjectsClientContract; constructor( indexPattern: string, isRollup: boolean, - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract ) { this._indexPattern = indexPattern; this._isRollup = isRollup; - this._mlClusterClient = mlClusterClient; + this._mlClusterClient = client; this._savedObjectsClient = savedObjectsClient; } private async loadFieldCaps(): Promise { - return this._mlClusterClient.callAsCurrentUser('fieldCaps', { + const { body } = await this._mlClusterClient.asCurrentUser.fieldCaps({ index: this._indexPattern, fields: '*', }); + return body; } // create field object from the results from _field_caps diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts index 38d6481e02a74..891cb2e0d1e64 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts @@ -21,28 +21,23 @@ describe('job_service - job_caps', () => { let savedObjectsClientMock: any; beforeEach(() => { - const callAsNonRollupMock = jest.fn((action: string) => { - switch (action) { - case 'fieldCaps': - return farequoteFieldCaps; - } - }); + const asNonRollupMock = { + fieldCaps: jest.fn(() => ({ body: farequoteFieldCaps })), + }; + mlClusterClientNonRollupMock = { - callAsCurrentUser: callAsNonRollupMock, - callAsInternalUser: callAsNonRollupMock, + asCurrentUser: asNonRollupMock, + asInternalUser: asNonRollupMock, + }; + + const callAsRollupMock = { + fieldCaps: jest.fn(() => ({ body: cloudwatchFieldCaps })), + rollup: { getRollupIndexCaps: jest.fn(() => Promise.resolve({ body: rollupCaps })) }, }; - const callAsRollupMock = jest.fn((action: string) => { - switch (action) { - case 'fieldCaps': - return cloudwatchFieldCaps; - case 'ml.rollupIndexCapabilities': - return Promise.resolve(rollupCaps); - } - }); mlClusterClientRollupMock = { - callAsCurrentUser: callAsRollupMock, - callAsInternalUser: callAsRollupMock, + asCurrentUser: callAsRollupMock, + asInternalUser: callAsRollupMock, }; savedObjectsClientMock = { diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts index 5616dade53a78..7559111d012d0 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; +import { IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; import { Aggregation, Field, NewJobCaps } from '../../../../common/types/fields'; import { fieldServiceProvider } from './field_service'; @@ -12,18 +12,13 @@ interface NewJobCapsResponse { [indexPattern: string]: NewJobCaps; } -export function newJobCapsProvider(mlClusterClient: ILegacyScopedClusterClient) { +export function newJobCapsProvider(client: IScopedClusterClient) { async function newJobCaps( indexPattern: string, isRollup: boolean = false, savedObjectsClient: SavedObjectsClientContract ): Promise { - const fieldService = fieldServiceProvider( - indexPattern, - isRollup, - mlClusterClient, - savedObjectsClient - ); + const fieldService = fieldServiceProvider(indexPattern, isRollup, client, savedObjectsClient); const { aggs, fields } = await fieldService.getData(); convertForStringify(aggs, fields); diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts b/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts index f3a9bd49c27d6..b7f4c8af62283 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/rollup.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { SavedObject } from 'kibana/server'; import { IndexPatternAttributes } from 'src/plugins/data/server'; import { SavedObjectsClientContract } from 'kibana/server'; @@ -22,7 +22,7 @@ export interface RollupJob { export async function rollupServiceProvider( indexPattern: string, - { callAsCurrentUser }: ILegacyScopedClusterClient, + { asCurrentUser }: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract ) { const rollupIndexPatternObject = await loadRollupIndexPattern(indexPattern, savedObjectsClient); @@ -32,8 +32,8 @@ export async function rollupServiceProvider( if (rollupIndexPatternObject !== null) { const parsedTypeMetaData = JSON.parse(rollupIndexPatternObject.attributes.typeMeta); const rollUpIndex: string = parsedTypeMetaData.params.rollup_index; - const rollupCaps = await callAsCurrentUser('ml.rollupIndexCapabilities', { - indexPattern: rollUpIndex, + const { body: rollupCaps } = await asCurrentUser.rollup.getRollupIndexCaps({ + index: rollUpIndex, }); const indexRollupCaps = rollupCaps[rollUpIndex]; diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts index 1c74953e4dda9..810d0ae9dcd87 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts @@ -4,48 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { validateJob, ValidateJobPayload } from './job_validation'; import { JobValidationMessage } from '../../../common/constants/messages'; -const mlClusterClient = ({ - // mock callAsCurrentUser - callAsCurrentUser: (method: string) => { - return new Promise((resolve) => { - if (method === 'fieldCaps') { - resolve({ fields: [] }); - return; - } else if (method === 'ml.info') { - resolve({ +const callAs = { + fieldCaps: () => Promise.resolve({ body: { fields: [] } }), + ml: { + info: () => + Promise.resolve({ + body: { limits: { effective_max_model_memory_limit: '100MB', max_model_memory_limit: '1GB', }, - }); - } - resolve({}); - }) as Promise; + }, + }), }, + search: () => Promise.resolve({ body: {} }), +}; - // mock callAsInternalUser - callAsInternalUser: (method: string) => { - return new Promise((resolve) => { - if (method === 'fieldCaps') { - resolve({ fields: [] }); - return; - } else if (method === 'ml.info') { - resolve({ - limits: { - effective_max_model_memory_limit: '100MB', - max_model_memory_limit: '1GB', - }, - }); - } - resolve({}); - }) as Promise; - }, -} as unknown) as ILegacyScopedClusterClient; +const mlClusterClient = ({ + asCurrentUser: callAs, + asInternalUser: callAs, +} as unknown) as IScopedClusterClient; // Note: The tests cast `payload` as any // so we can simulate possible runtime payloads diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index 6692ecb22bd9e..9e272f1f770fc 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import Boom from 'boom'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { fieldsServiceProvider } from '../fields_service'; import { renderTemplate } from '../../../common/util/string_utils'; @@ -34,7 +34,7 @@ export type ValidateJobPayload = TypeOf; * @kbn/config-schema has checked the payload {@link validateJobSchema}. */ export async function validateJob( - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, payload: ValidateJobPayload, kbnVersion = 'current', isSecurityDisabled?: boolean @@ -63,8 +63,8 @@ export async function validateJob( // if no duration was part of the request, fall back to finding out // the time range of the time field of the index, but also check first // if the time field is a valid field of type 'date' using isValidTimeField() - if (typeof duration === 'undefined' && (await isValidTimeField(mlClusterClient, job))) { - const fs = fieldsServiceProvider(mlClusterClient); + if (typeof duration === 'undefined' && (await isValidTimeField(client, job))) { + const fs = fieldsServiceProvider(client); const index = job.datafeed_config.indices.join(','); const timeField = job.data_description.time_field; const timeRange = await fs.getTimeFieldRange(index, timeField, job.datafeed_config.query); @@ -79,24 +79,22 @@ export async function validateJob( // next run only the cardinality tests to find out if they trigger an error // so we can decide later whether certain additional tests should be run - const cardinalityMessages = await validateCardinality(mlClusterClient, job); + const cardinalityMessages = await validateCardinality(client, job); validationMessages.push(...cardinalityMessages); const cardinalityError = cardinalityMessages.some((m) => { return messages[m.id as MessageId].status === VALIDATION_STATUS.ERROR; }); validationMessages.push( - ...(await validateBucketSpan(mlClusterClient, job, duration, isSecurityDisabled)) + ...(await validateBucketSpan(client, job, duration, isSecurityDisabled)) ); - validationMessages.push(...(await validateTimeRange(mlClusterClient, job, duration))); + validationMessages.push(...(await validateTimeRange(client, job, duration))); // only run the influencer and model memory limit checks // if cardinality checks didn't return a message with an error level if (cardinalityError === false) { validationMessages.push(...(await validateInfluencers(job))); - validationMessages.push( - ...(await validateModelMemoryLimit(mlClusterClient, job, duration)) - ); + validationMessages.push(...(await validateModelMemoryLimit(client, job, duration))); } } else { validationMessages = basicValidation.messages; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js index 11f8d8967c4e0..315ad09176571 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js @@ -45,7 +45,7 @@ const pickBucketSpan = (bucketSpans) => { return bucketSpans[i]; }; -export async function validateBucketSpan(mlClusterClient, job, duration) { +export async function validateBucketSpan(client, job, duration) { validateJobObject(job); // if there is no duration, do not run the estimate test @@ -117,7 +117,7 @@ export async function validateBucketSpan(mlClusterClient, job, duration) { try { const estimations = estimatorConfigs.map((data) => { return new Promise((resolve) => { - estimateBucketSpanFactory(mlClusterClient)(data) + estimateBucketSpanFactory(client)(data) .then(resolve) // this catch gets triggered when the estimation code runs without error // but isn't able to come up with a bucket span estimation. diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts index f9145ab576d71..80418d590af76 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.test.ts @@ -24,12 +24,12 @@ import mockItSearchResponse from './__mocks__/mock_it_search_response.json'; const mlClusterClientFactory = (mockSearchResponse: any) => { const callAs = () => { return new Promise((resolve) => { - resolve(mockSearchResponse); + resolve({ body: mockSearchResponse }); }); }; return { - callAsCurrentUser: callAs, - callAsInternalUser: callAs, + asCurrentUser: callAs, + asInternalUser: callAs, }; }; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts index 16ee70ad9efde..1be0751e15f22 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.test.ts @@ -6,7 +6,7 @@ import cloneDeep from 'lodash/cloneDeep'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; @@ -24,21 +24,21 @@ const mockResponses = { const mlClusterClientFactory = ( responses: Record, fail = false -): ILegacyScopedClusterClient => { - const callAs = (requestName: string) => { - return new Promise((resolve, reject) => { - const response = responses[requestName]; - if (fail) { - reject(response); - } else { - resolve(response); - } - }) as Promise; +): IScopedClusterClient => { + const callAs = { + search: () => Promise.resolve({ body: responses.search }), + fieldCaps: () => Promise.resolve({ body: responses.fieldCaps }), }; - return { - callAsCurrentUser: callAs, - callAsInternalUser: callAs, + + const callAsFail = { + search: () => Promise.reject({ body: {} }), + fieldCaps: () => Promise.reject({ body: {} }), }; + + return ({ + asCurrentUser: fail === false ? callAs : callAsFail, + asInternalUser: fail === false ? callAs : callAsFail, + } as unknown) as IScopedClusterClient; }; describe('ML - validateCardinality', () => { diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts index 1545c4c0062ec..c5822b863c83d 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { DataVisualizer } from '../data_visualizer'; import { validateJobObject } from './validate_job_object'; @@ -43,12 +43,9 @@ type Validator = (obj: { messages: Messages; }>; -const validateFactory = ( - mlClusterClient: ILegacyScopedClusterClient, - job: CombinedJob -): Validator => { - const { callAsCurrentUser } = mlClusterClient; - const dv = new DataVisualizer(mlClusterClient); +const validateFactory = (client: IScopedClusterClient, job: CombinedJob): Validator => { + const { asCurrentUser } = client; + const dv = new DataVisualizer(client); const modelPlotConfigTerms = job?.model_plot_config?.terms ?? ''; const modelPlotConfigFieldCount = @@ -77,7 +74,7 @@ const validateFactory = ( ] as string[]; // use fieldCaps endpoint to get data about whether fields are aggregatable - const fieldCaps = await callAsCurrentUser('fieldCaps', { + const { body: fieldCaps } = await asCurrentUser.fieldCaps({ index: job.datafeed_config.indices.join(','), fields: uniqueFieldNames, }); @@ -154,7 +151,7 @@ const validateFactory = ( }; export async function validateCardinality( - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, job?: CombinedJob ): Promise | never { const messages: Messages = []; @@ -174,7 +171,7 @@ export async function validateCardinality( } // validate({ type, isInvalid }) asynchronously returns an array of validation messages - const validate = validateFactory(mlClusterClient, job); + const validate = validateFactory(client, job); const modelPlotEnabled = job.model_plot_config?.enabled ?? false; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts index 6ffb0e320982b..35792c66e66ec 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.test.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { CombinedJob, Detector } from '../../../common/types/anomaly_detection_jobs'; -import { ModelMemoryEstimate } from '../calculate_model_memory_limit/calculate_model_memory_limit'; +import { ModelMemoryEstimateResponse } from '../calculate_model_memory_limit/calculate_model_memory_limit'; import { validateModelMemoryLimit } from './validate_model_memory_limit'; describe('ML - validateModelMemoryLimit', () => { @@ -65,44 +65,36 @@ describe('ML - validateModelMemoryLimit', () => { }; // mock estimate model memory - const modelMemoryEstimateResponse: ModelMemoryEstimate = { + const modelMemoryEstimateResponse: ModelMemoryEstimateResponse = { model_memory_estimate: '40mb', }; interface MockAPICallResponse { - 'ml.estimateModelMemory'?: ModelMemoryEstimate; + 'ml.estimateModelMemory'?: ModelMemoryEstimateResponse; } - // mock callAsCurrentUser + // mock asCurrentUser // used in three places: // - to retrieve the info endpoint // - to search for cardinality of split field // - to retrieve field capabilities used in search for split field cardinality const getMockMlClusterClient = ({ 'ml.estimateModelMemory': estimateModelMemory, - }: MockAPICallResponse = {}): ILegacyScopedClusterClient => { - const callAs = (call: string) => { - if (typeof call === undefined) { - return Promise.reject(); - } - - let response = {}; - if (call === 'ml.info') { - response = mlInfoResponse; - } else if (call === 'search') { - response = cardinalitySearchResponse; - } else if (call === 'fieldCaps') { - response = fieldCapsResponse; - } else if (call === 'ml.estimateModelMemory') { - response = estimateModelMemory || modelMemoryEstimateResponse; - } - return Promise.resolve(response); + }: MockAPICallResponse = {}): IScopedClusterClient => { + const callAs = { + ml: { + info: () => Promise.resolve({ body: mlInfoResponse }), + estimateModelMemory: () => + Promise.resolve({ body: estimateModelMemory || modelMemoryEstimateResponse }), + }, + search: () => Promise.resolve({ body: cardinalitySearchResponse }), + fieldCaps: () => Promise.resolve({ body: fieldCapsResponse }), }; - return { - callAsCurrentUser: callAs, - callAsInternalUser: callAs, - }; + return ({ + asCurrentUser: callAs, + asInternalUser: callAs, + } as unknown) as IScopedClusterClient; }; function getJobConfig(influencers: string[] = [], detectors: Detector[] = []) { diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts index 728342294c424..9733e17e0f379 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_model_memory_limit.ts @@ -5,7 +5,7 @@ */ import numeral from '@elastic/numeral'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { validateJobObject } from './validate_job_object'; import { calculateModelMemoryLimitProvider } from '../calculate_model_memory_limit'; @@ -16,11 +16,11 @@ import { MlInfoResponse } from '../../../common/types/ml_server_info'; const MODEL_MEMORY_LIMIT_MINIMUM_BYTES = 1048576; export async function validateModelMemoryLimit( - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, job: CombinedJob, duration?: { start?: number; end?: number } ) { - const { callAsInternalUser } = mlClusterClient; + const { asInternalUser } = client; validateJobObject(job); // retrieve the model memory limit specified by the user in the job config. @@ -52,12 +52,12 @@ export async function validateModelMemoryLimit( // retrieve the max_model_memory_limit value from the server // this will be unset unless the user has set this on their cluster - const info = (await callAsInternalUser('ml.info')) as MlInfoResponse; - const maxModelMemoryLimit = info.limits.max_model_memory_limit?.toUpperCase(); - const effectiveMaxModelMemoryLimit = info.limits.effective_max_model_memory_limit?.toUpperCase(); + const { body } = await asInternalUser.ml.info(); + const maxModelMemoryLimit = body.limits.max_model_memory_limit?.toUpperCase(); + const effectiveMaxModelMemoryLimit = body.limits.effective_max_model_memory_limit?.toUpperCase(); if (runCalcModelMemoryTest) { - const { modelMemoryLimit } = await calculateModelMemoryLimitProvider(mlClusterClient)( + const { modelMemoryLimit } = await calculateModelMemoryLimitProvider(client)( job.analysis_config, job.datafeed_config.indices.join(','), job.datafeed_config.query, diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts index a45be189ba3d8..12458af0521a9 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.test.ts @@ -6,7 +6,7 @@ import cloneDeep from 'lodash/cloneDeep'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; @@ -21,16 +21,15 @@ const mockSearchResponse = { search: mockTimeRange, }; -const mlClusterClientFactory = (resp: any): ILegacyScopedClusterClient => { - const callAs = (path: string) => { - return new Promise((resolve) => { - resolve(resp[path]); - }) as Promise; - }; - return { - callAsCurrentUser: callAs, - callAsInternalUser: callAs, +const mlClusterClientFactory = (response: any): IScopedClusterClient => { + const callAs = { + fieldCaps: () => Promise.resolve({ body: response.fieldCaps }), + search: () => Promise.resolve({ body: response.search }), }; + return ({ + asCurrentUser: callAs, + asInternalUser: callAs, + } as unknown) as IScopedClusterClient; }; function getMinimalValidJob() { diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts index a94ceffa90273..83d9621898f96 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_time_range.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/server'; import { parseInterval } from '../../../common/util/parse_interval'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; @@ -26,15 +26,12 @@ const BUCKET_SPAN_COMPARE_FACTOR = 25; const MIN_TIME_SPAN_MS = 7200000; const MIN_TIME_SPAN_READABLE = '2 hours'; -export async function isValidTimeField( - { callAsCurrentUser }: ILegacyScopedClusterClient, - job: CombinedJob -) { +export async function isValidTimeField({ asCurrentUser }: IScopedClusterClient, job: CombinedJob) { const index = job.datafeed_config.indices.join(','); const timeField = job.data_description.time_field; // check if time_field is of type 'date' or 'date_nanos' - const fieldCaps = await callAsCurrentUser('fieldCaps', { + const { body: fieldCaps } = await asCurrentUser.fieldCaps({ index, fields: [timeField], }); @@ -47,7 +44,7 @@ export async function isValidTimeField( } export async function validateTimeRange( - mlClientCluster: ILegacyScopedClusterClient, + mlClientCluster: IScopedClusterClient, job: CombinedJob, timeRange?: Partial ) { diff --git a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts index 9c0efe259844c..76dc68d2b59e3 100644 --- a/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts +++ b/x-pack/plugins/ml/server/models/results_service/get_partition_fields_values.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { PARTITION_FIELDS } from '../../../common/constants/anomalies'; import { PartitionFieldsType } from '../../../common/types/anomalies'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; @@ -74,9 +74,7 @@ function getFieldObject(fieldType: PartitionFieldsType, aggs: any) { : {}; } -export const getPartitionFieldsValuesFactory = ({ - callAsInternalUser, -}: ILegacyScopedClusterClient) => +export const getPartitionFieldsValuesFactory = ({ asInternalUser }: IScopedClusterClient) => /** * Gets the record of partition fields with possible values that fit the provided queries. * @param jobId - Job ID @@ -92,7 +90,7 @@ export const getPartitionFieldsValuesFactory = ({ earliestMs: number, latestMs: number ) { - const jobsResponse = await callAsInternalUser('ml.jobs', { jobId: [jobId] }); + const { body: jobsResponse } = await asInternalUser.ml.getJobs({ job_id: jobId }); if (jobsResponse.count === 0 || jobsResponse.jobs === undefined) { throw Boom.notFound(`Job with the id "${jobId}" not found`); } @@ -101,7 +99,7 @@ export const getPartitionFieldsValuesFactory = ({ const isModelPlotEnabled = job?.model_plot_config?.enabled; - const resp = await callAsInternalUser('search', { + const { body } = await asInternalUser.search({ index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -151,7 +149,7 @@ export const getPartitionFieldsValuesFactory = ({ return PARTITION_FIELDS.reduce((acc, key) => { return { ...acc, - ...getFieldObject(key, resp.aggregations), + ...getFieldObject(key, body.aggregations), }; }, {}); }; diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index 7be8bac61e69d..190b5d99309d7 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -9,7 +9,7 @@ import slice from 'lodash/slice'; import get from 'lodash/get'; import moment from 'moment'; import { SearchResponse } from 'elasticsearch'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import Boom from 'boom'; import { buildAnomalyTableItems } from './build_anomaly_table_items'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; @@ -40,8 +40,8 @@ interface Influencer { fieldValue: any; } -export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClient) { - const { callAsInternalUser } = mlClusterClient; +export function resultsServiceProvider(client: IScopedClusterClient) { + const { asInternalUser } = client; // Obtains data for the anomalies table, aggregating anomalies by day or hour as requested. // Return an Object with properties 'anomalies' and 'interval' (interval used to aggregate anomalies, // one of day, hour or second. Note 'auto' can be provided as the aggregationInterval in the request, @@ -144,7 +144,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }); } - const resp: SearchResponse = await callAsInternalUser('search', { + const { body } = await asInternalUser.search>({ index: ML_RESULTS_INDEX_PATTERN, rest_total_hits_as_int: true, size: maxRecords, @@ -178,9 +178,9 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie anomalies: [], interval: 'second', }; - if (resp.hits.total !== 0) { + if (body.hits.total !== 0) { let records: AnomalyRecordDoc[] = []; - resp.hits.hits.forEach((hit) => { + body.hits.hits.forEach((hit) => { records.push(hit._source); }); @@ -298,8 +298,8 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }, }; - const resp = await callAsInternalUser('search', query); - const maxScore = get(resp, ['aggregations', 'max_score', 'value'], null); + const { body } = await asInternalUser.search(query); + const maxScore = get(body, ['aggregations', 'max_score', 'value'], null); return { maxScore }; } @@ -336,7 +336,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie // Size of job terms agg, consistent with maximum number of jobs supported by Java endpoints. const maxJobs = 10000; - const resp = await callAsInternalUser('search', { + const { body } = await asInternalUser.search({ index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -364,7 +364,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }); const bucketsByJobId: Array<{ key: string; maxTimestamp: { value?: number } }> = get( - resp, + body, ['aggregations', 'byJobId', 'buckets'], [] ); @@ -380,7 +380,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie // from the given index and job ID. // Returned response consists of a list of examples against category ID. async function getCategoryExamples(jobId: string, categoryIds: any, maxExamples: number) { - const resp = await callAsInternalUser('search', { + const { body } = await asInternalUser.search({ index: ML_RESULTS_INDEX_PATTERN, rest_total_hits_as_int: true, size: ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, // Matches size of records in anomaly summary table. @@ -394,8 +394,8 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }); const examplesByCategoryId: { [key: string]: any } = {}; - if (resp.hits.total !== 0) { - resp.hits.hits.forEach((hit: any) => { + if (body.hits.total !== 0) { + body.hits.hits.forEach((hit: any) => { if (maxExamples) { examplesByCategoryId[hit._source.category_id] = slice( hit._source.examples, @@ -415,7 +415,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie // Returned response contains four properties - categoryId, regex, examples // and terms (space delimited String of the common tokens matched in values of the category). async function getCategoryDefinition(jobId: string, categoryId: string) { - const resp = await callAsInternalUser('search', { + const { body } = await asInternalUser.search({ index: ML_RESULTS_INDEX_PATTERN, rest_total_hits_as_int: true, size: 1, @@ -429,8 +429,8 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }); const definition = { categoryId, terms: null, regex: null, examples: [] }; - if (resp.hits.total !== 0) { - const source = resp.hits.hits[0]._source; + if (body.hits.total !== 0) { + const source = body.hits.hits[0]._source; definition.categoryId = source.category_id; definition.regex = source.regex; definition.terms = source.terms; @@ -456,7 +456,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }, }); } - const results: SearchResponse = await callAsInternalUser('search', { + const { body } = await asInternalUser.search>({ index: ML_RESULTS_INDEX_PATTERN, body: { query: { @@ -473,7 +473,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }, }, }); - return results ? results.hits.hits.map((r) => r._source) : []; + return body ? body.hits.hits.map((r) => r._source) : []; } async function getCategoryStoppedPartitions( @@ -485,15 +485,15 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }; // first determine from job config if stop_on_warn is true // if false return [] - const jobConfigResponse: MlJobsResponse = await callAsInternalUser('ml.jobs', { - jobId: jobIds, + const { body } = await asInternalUser.ml.getJobs({ + job_id: jobIds.join(), }); - if (!jobConfigResponse || jobConfigResponse.jobs.length < 1) { + if (!body || body.jobs.length < 1) { throw Boom.notFound(`Unable to find anomaly detector jobs ${jobIds.join(', ')}`); } - const jobIdsWithStopOnWarnSet = jobConfigResponse.jobs + const jobIdsWithStopOnWarnSet = body.jobs .filter( (jobConfig) => jobConfig.analysis_config?.per_partition_categorization?.stop_on_warn === true @@ -543,7 +543,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie }, }, ]; - const results: SearchResponse = await callAsInternalUser('search', { + const { body: results } = await asInternalUser.search>({ index: ML_RESULTS_INDEX_PATTERN, size: 0, body: { @@ -594,7 +594,7 @@ export function resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClie getCategoryExamples, getLatestBucketTimestampByJob, getMaxAnomalyScore, - getPartitionFieldsValues: getPartitionFieldsValuesFactory(mlClusterClient), + getPartitionFieldsValues: getPartitionFieldsValuesFactory(client), getCategorizerStats, getCategoryStoppedPartitions, }; diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 76128341e6ddc..39672f5b188bc 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -9,18 +9,16 @@ import { CoreSetup, CoreStart, Plugin, - ILegacyScopedClusterClient, KibanaRequest, Logger, PluginInitializerContext, - ILegacyCustomClusterClient, CapabilitiesStart, + IClusterClient, } from 'kibana/server'; import { PluginsSetup, RouteInitialization } from './types'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; import { MlCapabilities } from '../common/types/capabilities'; -import { elasticsearchJsPlugin } from './client/elasticsearch_ml'; import { initMlTelemetry } from './lib/telemetry'; import { initMlServerLog } from './client/log'; import { initSampleDataSets } from './lib/sample_data_sets'; @@ -50,17 +48,7 @@ import { setupCapabilitiesSwitcher } from './lib/capabilities'; import { registerKibanaSettings } from './lib/register_settings'; import { inferenceRoutes } from './routes/inference'; -declare module 'kibana/server' { - interface RequestHandlerContext { - [PLUGIN_ID]?: { - mlClient: ILegacyScopedClusterClient; - }; - } -} - -export interface MlPluginSetup extends SharedServices { - mlClient: ILegacyCustomClusterClient; -} +export type MlPluginSetup = SharedServices; export type MlPluginStart = void; export class MlServerPlugin implements Plugin { @@ -68,6 +56,7 @@ export class MlServerPlugin implements Plugin { - return { - mlClient: mlClient.asScoped(request), - }; - }); - const routeInit: RouteInitialization = { router: coreSetup.http.createRouter(), mlLicense: this.mlLicense, @@ -176,13 +154,19 @@ export class MlServerPlugin implements Plugin this.clusterClient + ), }; } public start(coreStart: CoreStart): MlPluginStart { this.capabilities = coreStart.capabilities; + this.clusterClient = coreStart.elasticsearch.client; } public stop() { diff --git a/x-pack/plugins/ml/server/routes/annotations.ts b/x-pack/plugins/ml/server/routes/annotations.ts index a6de80bb7e5e2..5c4b36164fbb0 100644 --- a/x-pack/plugins/ml/server/routes/annotations.ts +++ b/x-pack/plugins/ml/server/routes/annotations.ts @@ -58,9 +58,9 @@ export function annotationRoutes( tags: ['access:ml:canGetAnnotations'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { getAnnotations } = annotationServiceProvider(legacyClient); + const { getAnnotations } = annotationServiceProvider(client); const resp = await getAnnotations(request.body); return response.ok({ @@ -91,14 +91,14 @@ export function annotationRoutes( tags: ['access:ml:canCreateAnnotation'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable(legacyClient); + const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable(client); if (annotationsFeatureAvailable === false) { throw getAnnotationsFeatureUnavailableErrorMessage(); } - const { indexAnnotation } = annotationServiceProvider(legacyClient); + const { indexAnnotation } = annotationServiceProvider(client); const currentUser = securityPlugin !== undefined ? securityPlugin.authc.getCurrentUser(request) : {}; @@ -134,15 +134,15 @@ export function annotationRoutes( tags: ['access:ml:canDeleteAnnotation'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable(legacyClient); + const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable(client); if (annotationsFeatureAvailable === false) { throw getAnnotationsFeatureUnavailableErrorMessage(); } const annotationId = request.params.annotationId; - const { deleteAnnotation } = annotationServiceProvider(legacyClient); + const { deleteAnnotation } = annotationServiceProvider(client); const resp = await deleteAnnotation(annotationId); return response.ok({ diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 0027bec910134..251e465eafccc 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -5,6 +5,7 @@ */ import { schema } from '@kbn/config-schema'; +import { RequestParams } from '@elastic/elasticsearch'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -20,6 +21,7 @@ import { getModelSnapshotsSchema, updateModelSnapshotSchema, } from './schemas/anomaly_detectors_schema'; + /** * Routes for the anomaly detectors */ @@ -42,11 +44,11 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ response, client }) => { try { - const results = await legacyClient.callAsInternalUser('ml.jobs'); + const { body } = await client.asInternalUser.ml.getJobs(); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -73,12 +75,12 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { jobId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.jobs', { jobId }); + const { body } = await client.asInternalUser.ml.getJobs({ job_id: jobId }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -104,11 +106,11 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.jobStats'); + const { body } = await client.asInternalUser.ml.getJobStats(); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -135,12 +137,12 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { jobId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.jobStats', { jobId }); + const { body } = await client.asInternalUser.ml.getJobStats({ job_id: jobId }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -171,15 +173,15 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { jobId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.addJob', { - jobId, + const { body } = await client.asInternalUser.ml.putJob({ + job_id: jobId, body: request.body, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -208,15 +210,15 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canUpdateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { jobId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.updateJob', { - jobId, + const { body } = await client.asInternalUser.ml.updateJob({ + job_id: jobId, body: request.body, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -243,14 +245,12 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canOpenJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { jobId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.openJob', { - jobId, - }); + const { body } = await client.asInternalUser.ml.openJob({ job_id: jobId }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -277,18 +277,18 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCloseJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const options: { jobId: string; force?: boolean } = { - jobId: request.params.jobId, + const options: RequestParams.MlCloseJob = { + job_id: request.params.jobId, }; const force = request.query.force; if (force !== undefined) { options.force = force; } - const results = await legacyClient.callAsInternalUser('ml.closeJob', options); + const { body } = await client.asInternalUser.ml.closeJob(options); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -315,18 +315,19 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canDeleteJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const options: { jobId: string; force?: boolean } = { - jobId: request.params.jobId, + const options: RequestParams.MlDeleteJob = { + job_id: request.params.jobId, + wait_for_completion: false, }; const force = request.query.force; if (force !== undefined) { options.force = force; } - const results = await legacyClient.callAsInternalUser('ml.deleteJob', options); + const { body } = await client.asInternalUser.ml.deleteJob(options); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -351,13 +352,11 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.validateDetector', { - body: request.body, - }); + const { body } = await client.asInternalUser.ml.validateDetector({ body: request.body }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -386,16 +385,16 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canForecastJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const jobId = request.params.jobId; const duration = request.body.duration; - const results = await legacyClient.callAsInternalUser('ml.forecast', { - jobId, + const { body } = await client.asInternalUser.ml.forecast({ + job_id: jobId, duration, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -427,14 +426,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.records', { - jobId: request.params.jobId, + const { body } = await client.asInternalUser.ml.getRecords({ + job_id: request.params.jobId, body: request.body, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -466,15 +465,15 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.buckets', { - jobId: request.params.jobId, + const { body } = await client.asInternalUser.ml.getBuckets({ + job_id: request.params.jobId, timestamp: request.params.timestamp, body: request.body, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -506,17 +505,17 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.overallBuckets', { - jobId: request.params.jobId, + const { body } = await client.asInternalUser.ml.getOverallBuckets({ + job_id: request.params.jobId, top_n: request.body.topN, bucket_span: request.body.bucketSpan, start: request.body.start, end: request.body.end, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -543,14 +542,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.categories', { - jobId: request.params.jobId, - categoryId: request.params.categoryId, + const { body } = await client.asInternalUser.ml.getCategories({ + job_id: request.params.jobId, + category_id: request.params.categoryId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -577,13 +576,13 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.modelSnapshots', { - jobId: request.params.jobId, + const { body } = await client.asInternalUser.ml.getModelSnapshots({ + job_id: request.params.jobId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -610,14 +609,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.modelSnapshots', { - jobId: request.params.jobId, - snapshotId: request.params.snapshotId, + const { body } = await client.asInternalUser.ml.getModelSnapshots({ + job_id: request.params.jobId, + snapshot_id: request.params.snapshotId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -646,15 +645,15 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.updateModelSnapshot', { - jobId: request.params.jobId, - snapshotId: request.params.snapshotId, + const { body } = await client.asInternalUser.ml.updateModelSnapshot({ + job_id: request.params.jobId, + snapshot_id: request.params.snapshotId, body: request.body, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -681,14 +680,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.deleteModelSnapshot', { - jobId: request.params.jobId, - snapshotId: request.params.snapshotId, + const { body } = await client.asInternalUser.ml.deleteModelSnapshot({ + job_id: request.params.jobId, + snapshot_id: request.params.snapshotId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json index e3108b8c759f4..6d01a853698b8 100644 --- a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.json", + "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { "outDir": "./target", "target": "es6", diff --git a/x-pack/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts index 3beb6e437b2ee..2c95ce6fb59ec 100644 --- a/x-pack/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -4,43 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { calendarSchema, calendarIdSchema, calendarIdsSchema } from './schemas/calendars_schema'; import { CalendarManager, Calendar, FormCalendar } from '../models/calendar'; -function getAllCalendars(legacyClient: ILegacyScopedClusterClient) { - const cal = new CalendarManager(legacyClient); +function getAllCalendars(client: IScopedClusterClient) { + const cal = new CalendarManager(client); return cal.getAllCalendars(); } -function getCalendar(legacyClient: ILegacyScopedClusterClient, calendarId: string) { - const cal = new CalendarManager(legacyClient); +function getCalendar(client: IScopedClusterClient, calendarId: string) { + const cal = new CalendarManager(client); return cal.getCalendar(calendarId); } -function newCalendar(legacyClient: ILegacyScopedClusterClient, calendar: FormCalendar) { - const cal = new CalendarManager(legacyClient); +function newCalendar(client: IScopedClusterClient, calendar: FormCalendar) { + const cal = new CalendarManager(client); return cal.newCalendar(calendar); } -function updateCalendar( - legacyClient: ILegacyScopedClusterClient, - calendarId: string, - calendar: Calendar -) { - const cal = new CalendarManager(legacyClient); +function updateCalendar(client: IScopedClusterClient, calendarId: string, calendar: Calendar) { + const cal = new CalendarManager(client); return cal.updateCalendar(calendarId, calendar); } -function deleteCalendar(legacyClient: ILegacyScopedClusterClient, calendarId: string) { - const cal = new CalendarManager(legacyClient); +function deleteCalendar(client: IScopedClusterClient, calendarId: string) { + const cal = new CalendarManager(client); return cal.deleteCalendar(calendarId); } -function getCalendarsByIds(legacyClient: ILegacyScopedClusterClient, calendarIds: string) { - const cal = new CalendarManager(legacyClient); +function getCalendarsByIds(client: IScopedClusterClient, calendarIds: string) { + const cal = new CalendarManager(client); return cal.getCalendarsByIds(calendarIds); } @@ -60,9 +56,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetCalendars'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const resp = await getAllCalendars(legacyClient); + const resp = await getAllCalendars(client); return response.ok({ body: resp, @@ -92,15 +88,15 @@ export function calendars({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetCalendars'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { let returnValue; try { const calendarIds = request.params.calendarIds.split(','); if (calendarIds.length === 1) { - returnValue = await getCalendar(legacyClient, calendarIds[0]); + returnValue = await getCalendar(client, calendarIds[0]); } else { - returnValue = await getCalendarsByIds(legacyClient, calendarIds); + returnValue = await getCalendarsByIds(client, calendarIds); } return response.ok({ @@ -131,10 +127,10 @@ export function calendars({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateCalendar'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const body = request.body; - const resp = await newCalendar(legacyClient, body); + const resp = await newCalendar(client, body); return response.ok({ body: resp, @@ -166,11 +162,11 @@ export function calendars({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateCalendar'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { calendarId } = request.params; const body = request.body; - const resp = await updateCalendar(legacyClient, calendarId, body); + const resp = await updateCalendar(client, calendarId, body); return response.ok({ body: resp, @@ -200,10 +196,10 @@ export function calendars({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canDeleteCalendar'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { calendarId } = request.params; - const resp = await deleteCalendar(legacyClient, calendarId); + const resp = await deleteCalendar(client, calendarId); return response.ok({ body: resp, diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 75d48056cf458..dea4803e8275e 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext, ILegacyScopedClusterClient } from 'kibana/server'; +import { RequestHandlerContext, IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages'; import { RouteInitialization } from '../types'; @@ -36,13 +36,14 @@ function deleteDestIndexPatternById(context: RequestHandlerContext, indexPattern */ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitialization) { async function userCanDeleteIndex( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, destinationIndex: string ): Promise { if (!mlLicense.isSecurityEnabled()) { return true; } - const privilege = await legacyClient.callAsCurrentUser('ml.privilegeCheck', { + + const { body } = await client.asCurrentUser.security.hasPrivileges({ body: { index: [ { @@ -52,10 +53,8 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat ], }, }); - if (!privilege) { - return false; - } - return privilege.has_all_requested === true; + + return body?.has_all_requested === true; } /** @@ -76,11 +75,11 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.getDataFrameAnalytics'); + const { body } = await client.asInternalUser.ml.getDataFrameAnalytics({ size: 1000 }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -107,14 +106,14 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { analyticsId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.getDataFrameAnalytics', { - analyticsId, + const { body } = await client.asInternalUser.ml.getDataFrameAnalytics({ + id: analyticsId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -137,11 +136,11 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.getDataFrameAnalyticsStats'); + const { body } = await client.asInternalUser.ml.getDataFrameAnalyticsStats({ size: 1000 }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -168,14 +167,14 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { analyticsId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.getDataFrameAnalyticsStats', { - analyticsId, + const { body } = await client.asInternalUser.ml.getDataFrameAnalyticsStats({ + id: analyticsId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -205,16 +204,18 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canCreateDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { analyticsId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.createDataFrameAnalytics', { - body: request.body, - analyticsId, - ...getAuthorizationHeader(request), - }); + const { body } = await client.asInternalUser.ml.putDataFrameAnalytics( + { + id: analyticsId, + body: request.body, + }, + getAuthorizationHeader(request) + ); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -241,14 +242,16 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.evaluateDataFrameAnalytics', { - body: request.body, - ...getAuthorizationHeader(request), - }); + const { body } = await client.asInternalUser.ml.evaluateDataFrame( + { + body: request.body, + }, + getAuthorizationHeader(request) + ); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -276,13 +279,13 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canCreateDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const results = await legacyClient.callAsInternalUser('ml.explainDataFrameAnalytics', { + const { body } = await client.asInternalUser.ml.explainDataFrameAnalytics({ body: request.body, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -310,7 +313,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canDeleteDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response, context }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response, context }) => { try { const { analyticsId } = request.params; const { deleteDestIndex, deleteDestIndexPattern } = request.query; @@ -324,11 +327,11 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat // Check if analyticsId is valid and get destination index if (deleteDestIndex || deleteDestIndexPattern) { try { - const dfa = await legacyClient.callAsInternalUser('ml.getDataFrameAnalytics', { - analyticsId, + const { body } = await client.asInternalUser.ml.getDataFrameAnalytics({ + id: analyticsId, }); - if (Array.isArray(dfa.data_frame_analytics) && dfa.data_frame_analytics.length > 0) { - destinationIndex = dfa.data_frame_analytics[0].dest.index; + if (Array.isArray(body.data_frame_analytics) && body.data_frame_analytics.length > 0) { + destinationIndex = body.data_frame_analytics[0].dest.index; } } catch (e) { return response.customError(wrapError(e)); @@ -337,11 +340,11 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat // If user checks box to delete the destinationIndex associated with the job if (destinationIndex && deleteDestIndex) { // Verify if user has privilege to delete the destination index - const userCanDeleteDestIndex = await userCanDeleteIndex(legacyClient, destinationIndex); + const userCanDeleteDestIndex = await userCanDeleteIndex(client, destinationIndex); // If user does have privilege to delete the index, then delete the index if (userCanDeleteDestIndex) { try { - await legacyClient.callAsCurrentUser('indices.delete', { + await client.asCurrentUser.indices.delete({ index: destinationIndex, }); destIndexDeleted.success = true; @@ -370,8 +373,8 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat // Delete the data frame analytics try { - await legacyClient.callAsInternalUser('ml.deleteDataFrameAnalytics', { - analyticsId, + await client.asInternalUser.ml.deleteDataFrameAnalytics({ + id: analyticsId, }); analyticsJobDeleted.success = true; } catch (deleteDFAError) { @@ -413,14 +416,14 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canStartStopDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { analyticsId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.startDataFrameAnalytics', { - analyticsId, + const { body } = await client.asInternalUser.ml.startDataFrameAnalytics({ + id: analyticsId, }); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -449,10 +452,10 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canStartStopDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const options: { analyticsId: string; force?: boolean | undefined } = { - analyticsId: request.params.analyticsId, + const options: { id: string; force?: boolean | undefined } = { + id: request.params.analyticsId, }; // @ts-expect-error TODO: update types if (request.url?.query?.force !== undefined) { @@ -460,9 +463,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat options.force = request.url.query.force; } - const results = await legacyClient.callAsInternalUser('ml.stopDataFrameAnalytics', options); + const { body } = await client.asInternalUser.ml.stopDataFrameAnalytics(options); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -490,16 +493,18 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canCreateDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { analyticsId } = request.params; - const results = await legacyClient.callAsInternalUser('ml.updateDataFrameAnalytics', { - body: request.body, - analyticsId, - ...getAuthorizationHeader(request), - }); + const { body } = await client.asInternalUser.ml.updateDataFrameAnalytics( + { + id: analyticsId, + body: request.body, + }, + getAuthorizationHeader(request) + ); return response.ok({ - body: results, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -526,10 +531,10 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canGetDataFrameAnalytics'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { analyticsId } = request.params; - const { getAnalyticsAuditMessages } = analyticsAuditMessagesProvider(legacyClient); + const { getAnalyticsAuditMessages } = analyticsAuditMessagesProvider(client); const results = await getAnalyticsAuditMessages(analyticsId); return response.ok({ diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts index 6355285127f06..a697fe017f192 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { DataVisualizer } from '../models/data_visualizer'; import { Field, HistogramField } from '../models/data_visualizer/data_visualizer'; @@ -17,7 +17,7 @@ import { import { RouteInitialization } from '../types'; function getOverallStats( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, indexPatternTitle: string, query: object, aggregatableFields: string[], @@ -27,7 +27,7 @@ function getOverallStats( earliestMs: number, latestMs: number ) { - const dv = new DataVisualizer(legacyClient); + const dv = new DataVisualizer(client); return dv.getOverallStats( indexPatternTitle, query, @@ -41,7 +41,7 @@ function getOverallStats( } function getStatsForFields( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, indexPatternTitle: string, query: any, fields: Field[], @@ -52,7 +52,7 @@ function getStatsForFields( interval: number, maxExamples: number ) { - const dv = new DataVisualizer(legacyClient); + const dv = new DataVisualizer(client); return dv.getStatsForFields( indexPatternTitle, query, @@ -67,13 +67,13 @@ function getStatsForFields( } function getHistogramsForFields( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, indexPatternTitle: string, query: any, fields: HistogramField[], samplerShardSize: number ) { - const dv = new DataVisualizer(legacyClient); + const dv = new DataVisualizer(client); return dv.getHistogramsForFields(indexPatternTitle, query, fields, samplerShardSize); } @@ -104,7 +104,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { const { params: { indexPatternTitle }, @@ -112,7 +112,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) } = request; const results = await getHistogramsForFields( - legacyClient, + client, indexPatternTitle, query, fields, @@ -151,7 +151,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { const { params: { indexPatternTitle }, @@ -168,7 +168,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) } = request; const results = await getStatsForFields( - legacyClient, + client, indexPatternTitle, query, fields, @@ -216,7 +216,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { const { params: { indexPatternTitle }, @@ -232,7 +232,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) } = request; const results = await getOverallStats( - legacyClient, + client, indexPatternTitle, query, aggregatableFields, diff --git a/x-pack/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts index 47a9afc2244d9..df2aa9e79d71b 100644 --- a/x-pack/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestParams } from '@elastic/elasticsearch'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -33,12 +34,12 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetDatafeeds'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const resp = await legacyClient.callAsInternalUser('ml.datafeeds'); + const { body } = await client.asInternalUser.ml.getDatafeeds(); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -65,13 +66,13 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetDatafeeds'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; - const resp = await legacyClient.callAsInternalUser('ml.datafeeds', { datafeedId }); + const { body } = await client.asInternalUser.ml.getDatafeeds({ datafeed_id: datafeedId }); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -94,12 +95,12 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetDatafeeds'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await legacyClient.callAsInternalUser('ml.datafeedStats'); + const { body } = await client.asInternalUser.ml.getDatafeedStats(); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -126,15 +127,15 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetDatafeeds'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; - const resp = await legacyClient.callAsInternalUser('ml.datafeedStats', { - datafeedId, + const { body } = await client.asInternalUser.ml.getDatafeedStats({ + datafeed_id: datafeedId, }); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -163,17 +164,19 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; - const resp = await legacyClient.callAsInternalUser('ml.addDatafeed', { - datafeedId, - body: request.body, - ...getAuthorizationHeader(request), - }); + const { body } = await client.asInternalUser.ml.putDatafeed( + { + datafeed_id: datafeedId, + body: request.body, + }, + getAuthorizationHeader(request) + ); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -202,17 +205,19 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canUpdateDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; - const resp = await legacyClient.callAsInternalUser('ml.updateDatafeed', { - datafeedId, - body: request.body, - ...getAuthorizationHeader(request), - }); + const { body } = await client.asInternalUser.ml.updateDatafeed( + { + datafeed_id: datafeedId, + body: request.body, + }, + getAuthorizationHeader(request) + ); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -241,20 +246,20 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canDeleteDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const options: { datafeedId: string; force?: boolean } = { - datafeedId: request.params.jobId, + const options: RequestParams.MlDeleteDatafeed = { + datafeed_id: request.params.jobId, }; const force = request.query.force; if (force !== undefined) { options.force = force; } - const resp = await legacyClient.callAsInternalUser('ml.deleteDatafeed', options); + const { body } = await client.asInternalUser.ml.deleteDatafeed(options); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -283,19 +288,19 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canStartStopDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; const { start, end } = request.body; - const resp = await legacyClient.callAsInternalUser('ml.startDatafeed', { - datafeedId, + const { body } = await client.asInternalUser.ml.startDatafeed({ + datafeed_id: datafeedId, start, end, }); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -322,16 +327,16 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canStartStopDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; - const resp = await legacyClient.callAsInternalUser('ml.stopDatafeed', { - datafeedId, + const { body } = await client.asInternalUser.ml.stopDatafeed({ + datafeed_id: datafeedId, }); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); @@ -358,16 +363,18 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canPreviewDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const datafeedId = request.params.datafeedId; - const resp = await legacyClient.callAsInternalUser('ml.datafeedPreview', { - datafeedId, - ...getAuthorizationHeader(request), - }); + const { body } = await client.asInternalUser.ml.previewDatafeed( + { + datafeed_id: datafeedId, + }, + getAuthorizationHeader(request) + ); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index 0595b31d5bbbc..e1bd8a5736d82 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -13,14 +13,14 @@ import { } from './schemas/fields_service_schema'; import { fieldsServiceProvider } from '../models/fields_service'; -function getCardinalityOfFields(legacyClient: ILegacyScopedClusterClient, payload: any) { - const fs = fieldsServiceProvider(legacyClient); +function getCardinalityOfFields(client: IScopedClusterClient, payload: any) { + const fs = fieldsServiceProvider(client); const { index, fieldNames, query, timeFieldName, earliestMs, latestMs } = payload; return fs.getCardinalityOfFields(index, fieldNames, query, timeFieldName, earliestMs, latestMs); } -function getTimeFieldRange(legacyClient: ILegacyScopedClusterClient, payload: any) { - const fs = fieldsServiceProvider(legacyClient); +function getTimeFieldRange(client: IScopedClusterClient, payload: any) { + const fs = fieldsServiceProvider(client); const { index, timeFieldName, query } = payload; return fs.getTimeFieldRange(index, timeFieldName, query); } @@ -50,9 +50,9 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canAccessML'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getCardinalityOfFields(legacyClient, request.body); + const resp = await getCardinalityOfFields(client, request.body); return response.ok({ body: resp, @@ -85,9 +85,9 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getTimeFieldRange(legacyClient, request.body); + const resp = await getTimeFieldRange(client, request.body); return response.ok({ body: resp, diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index 88949fecbc7df..4c1ee87e96fc5 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -5,7 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { MAX_FILE_SIZE_BYTES } from '../../common/constants/file_datavisualizer'; import { InputOverrides, @@ -28,17 +28,13 @@ import { importFileQuerySchema, } from './schemas/file_data_visualizer_schema'; -function analyzeFiles( - legacyClient: ILegacyScopedClusterClient, - data: InputData, - overrides: InputOverrides -) { - const { analyzeFile } = fileDataVisualizerProvider(legacyClient); +function analyzeFiles(client: IScopedClusterClient, data: InputData, overrides: InputOverrides) { + const { analyzeFile } = fileDataVisualizerProvider(client); return analyzeFile(data, overrides); } function importData( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, id: string, index: string, settings: Settings, @@ -46,7 +42,7 @@ function importData( ingestPipeline: IngestPipelineWrapper, data: InputData ) { - const { importData: importDataFunc } = importDataProvider(legacyClient); + const { importData: importDataFunc } = importDataProvider(client); return importDataFunc(id, index, settings, mappings, ingestPipeline, data); } @@ -78,9 +74,9 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canFindFileStructure'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { - const result = await analyzeFiles(legacyClient, request.body, request.query); + const result = await analyzeFiles(client, request.body, request.query); return response.ok({ body: result }); } catch (e) { return response.customError(wrapError(e)); @@ -113,7 +109,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat tags: ['access:ml:canFindFileStructure'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { const { id } = request.query; const { index, data, settings, mappings, ingestPipeline } = request.body; @@ -126,7 +122,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat } const result = await importData( - legacyClient, + client, id, index, settings, diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts index bb4f8a2bebaa9..efb4acfa432f9 100644 --- a/x-pack/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { createFilterSchema, filterIdSchema, updateFilterSchema } from './schemas/filters_schema'; @@ -12,37 +12,33 @@ import { FilterManager, FormFilter } from '../models/filter'; // TODO - add function for returning a list of just the filter IDs. // TODO - add function for returning a list of filter IDs plus item count. -function getAllFilters(legacyClient: ILegacyScopedClusterClient) { - const mgr = new FilterManager(legacyClient); +function getAllFilters(client: IScopedClusterClient) { + const mgr = new FilterManager(client); return mgr.getAllFilters(); } -function getAllFilterStats(legacyClient: ILegacyScopedClusterClient) { - const mgr = new FilterManager(legacyClient); +function getAllFilterStats(client: IScopedClusterClient) { + const mgr = new FilterManager(client); return mgr.getAllFilterStats(); } -function getFilter(legacyClient: ILegacyScopedClusterClient, filterId: string) { - const mgr = new FilterManager(legacyClient); +function getFilter(client: IScopedClusterClient, filterId: string) { + const mgr = new FilterManager(client); return mgr.getFilter(filterId); } -function newFilter(legacyClient: ILegacyScopedClusterClient, filter: FormFilter) { - const mgr = new FilterManager(legacyClient); +function newFilter(client: IScopedClusterClient, filter: FormFilter) { + const mgr = new FilterManager(client); return mgr.newFilter(filter); } -function updateFilter( - legacyClient: ILegacyScopedClusterClient, - filterId: string, - filter: FormFilter -) { - const mgr = new FilterManager(legacyClient); +function updateFilter(client: IScopedClusterClient, filterId: string, filter: FormFilter) { + const mgr = new FilterManager(client); return mgr.updateFilter(filterId, filter); } -function deleteFilter(legacyClient: ILegacyScopedClusterClient, filterId: string) { - const mgr = new FilterManager(legacyClient); +function deleteFilter(client: IScopedClusterClient, filterId: string) { + const mgr = new FilterManager(client); return mgr.deleteFilter(filterId); } @@ -65,9 +61,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetFilters'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const resp = await getAllFilters(legacyClient); + const resp = await getAllFilters(client); return response.ok({ body: resp, @@ -100,9 +96,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetFilters'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getFilter(legacyClient, request.params.filterId); + const resp = await getFilter(client, request.params.filterId); return response.ok({ body: resp, }); @@ -134,10 +130,10 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateFilter'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const body = request.body; - const resp = await newFilter(legacyClient, body); + const resp = await newFilter(client, body); return response.ok({ body: resp, @@ -172,11 +168,11 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateFilter'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { filterId } = request.params; const body = request.body; - const resp = await updateFilter(legacyClient, filterId, body); + const resp = await updateFilter(client, filterId, body); return response.ok({ body: resp, @@ -206,10 +202,10 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canDeleteFilter'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { filterId } = request.params; - const resp = await deleteFilter(legacyClient, filterId); + const resp = await deleteFilter(client, filterId); return response.ok({ body: resp, @@ -239,9 +235,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetFilters'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const resp = await getAllFilterStats(legacyClient); + const resp = await getAllFilterStats(client); return response.ok({ body: resp, diff --git a/x-pack/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts index 6a759cb97f308..ee817c492dbd4 100644 --- a/x-pack/plugins/ml/server/routes/indices.ts +++ b/x-pack/plugins/ml/server/routes/indices.ts @@ -31,7 +31,7 @@ export function indicesRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canAccessML'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { body: { index, fields: requestFields }, @@ -40,8 +40,8 @@ export function indicesRoutes({ router, mlLicense }: RouteInitialization) { requestFields !== undefined && Array.isArray(requestFields) ? requestFields.join(',') : '*'; - const result = await legacyClient.callAsCurrentUser('fieldCaps', { index, fields }); - return response.ok({ body: result }); + const { body } = await client.asInternalUser.fieldCaps({ index, fields }); + return response.ok({ body }); } catch (e) { return response.customError(wrapError(e)); } diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 2313decfabd5b..0c90081f8e755 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -37,9 +37,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { getJobAuditMessages } = jobAuditMessagesProvider(legacyClient); + const { getJobAuditMessages } = jobAuditMessagesProvider(client); const { jobId } = request.params; const { from } = request.query; const resp = await getJobAuditMessages(jobId, from); @@ -72,9 +72,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { getJobAuditMessages } = jobAuditMessagesProvider(legacyClient); + const { getJobAuditMessages } = jobAuditMessagesProvider(client); const { from } = request.query; const resp = await getJobAuditMessages(undefined, from); diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 3d560fc857e95..3c7f35b871b10 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -48,9 +48,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canStartStopDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { forceStartDatafeeds } = jobServiceProvider(legacyClient); + const { forceStartDatafeeds } = jobServiceProvider(client); const { datafeedIds, start, end } = request.body; const resp = await forceStartDatafeeds(datafeedIds, start, end); @@ -82,9 +82,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canStartStopDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { stopDatafeeds } = jobServiceProvider(legacyClient); + const { stopDatafeeds } = jobServiceProvider(client); const { datafeedIds } = request.body; const resp = await stopDatafeeds(datafeedIds); @@ -116,9 +116,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canDeleteJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { deleteJobs } = jobServiceProvider(legacyClient); + const { deleteJobs } = jobServiceProvider(client); const { jobIds } = request.body; const resp = await deleteJobs(jobIds); @@ -150,9 +150,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCloseJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { closeJobs } = jobServiceProvider(legacyClient); + const { closeJobs } = jobServiceProvider(client); const { jobIds } = request.body; const resp = await closeJobs(jobIds); @@ -184,9 +184,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCloseJob', 'access:ml:canStartStopDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { forceStopAndCloseJob } = jobServiceProvider(legacyClient); + const { forceStopAndCloseJob } = jobServiceProvider(client); const { jobId } = request.body; const resp = await forceStopAndCloseJob(jobId); @@ -223,9 +223,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { jobsSummary } = jobServiceProvider(legacyClient); + const { jobsSummary } = jobServiceProvider(client); const { jobIds } = request.body; const resp = await jobsSummary(jobIds); @@ -257,9 +257,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const { jobsWithTimerange } = jobServiceProvider(legacyClient); + const { jobsWithTimerange } = jobServiceProvider(client); const resp = await jobsWithTimerange(); return response.ok({ @@ -290,9 +290,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { createFullJobsList } = jobServiceProvider(legacyClient); + const { createFullJobsList } = jobServiceProvider(client); const { jobIds } = request.body; const resp = await createFullJobsList(jobIds); @@ -320,9 +320,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const { getAllGroups } = jobServiceProvider(legacyClient); + const { getAllGroups } = jobServiceProvider(client); const resp = await getAllGroups(); return response.ok({ @@ -353,9 +353,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canUpdateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { updateGroups } = jobServiceProvider(legacyClient); + const { updateGroups } = jobServiceProvider(client); const { jobs } = request.body; const resp = await updateGroups(jobs); @@ -383,9 +383,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const { deletingJobTasks } = jobServiceProvider(legacyClient); + const { deletingJobTasks } = jobServiceProvider(client); const resp = await deletingJobTasks(); return response.ok({ @@ -416,9 +416,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { jobsExist } = jobServiceProvider(legacyClient); + const { jobsExist } = jobServiceProvider(client); const { jobIds } = request.body; const resp = await jobsExist(jobIds); @@ -449,12 +449,12 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response, context }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response, context }) => { try { const { indexPattern } = request.params; const isRollup = request.query.rollup === 'true'; const savedObjectsClient = context.core.savedObjects.client; - const { newJobCaps } = jobServiceProvider(legacyClient); + const { newJobCaps } = jobServiceProvider(client); const resp = await newJobCaps(indexPattern, isRollup, savedObjectsClient); return response.ok({ @@ -485,7 +485,7 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { indexPatternTitle, @@ -499,7 +499,7 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { splitFieldValue, } = request.body; - const { newJobLineChart } = jobServiceProvider(legacyClient); + const { newJobLineChart } = jobServiceProvider(client); const resp = await newJobLineChart( indexPatternTitle, timeField, @@ -540,7 +540,7 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { const { indexPatternTitle, @@ -553,7 +553,7 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { splitFieldName, } = request.body; - const { newJobPopulationChart } = jobServiceProvider(legacyClient); + const { newJobPopulationChart } = jobServiceProvider(client); const resp = await newJobPopulationChart( indexPatternTitle, timeField, @@ -589,9 +589,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const { getAllJobAndGroupIds } = jobServiceProvider(legacyClient); + const { getAllJobAndGroupIds } = jobServiceProvider(client); const resp = await getAllJobAndGroupIds(); return response.ok({ @@ -622,9 +622,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { getLookBackProgress } = jobServiceProvider(legacyClient); + const { getLookBackProgress } = jobServiceProvider(client); const { jobId, start, end } = request.body; const resp = await getLookBackProgress(jobId, start, end); @@ -656,9 +656,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { validateCategoryExamples } = categorizationExamplesProvider(legacyClient); + const { validateCategoryExamples } = categorizationExamplesProvider(client); const { indexPatternTitle, timeField, @@ -709,9 +709,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { topCategories } = jobServiceProvider(legacyClient); + const { topCategories } = jobServiceProvider(client); const { jobId, count } = request.body; const resp = await topCategories(jobId, count); @@ -743,9 +743,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob', 'access:ml:canStartStopDatafeed'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const { revertModelSnapshot } = jobServiceProvider(legacyClient); + const { revertModelSnapshot } = jobServiceProvider(client); const { jobId, snapshotId, diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index 6da052663a002..b52043595327b 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { AnalysisConfig } from '../../common/types/anomaly_detection_jobs'; import { wrapError } from '../client/error_wrapper'; @@ -27,12 +27,12 @@ type CalculateModelMemoryLimitPayload = TypeOf; */ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, version: string) { function calculateModelMemoryLimit( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, payload: CalculateModelMemoryLimitPayload ) { const { analysisConfig, indexPattern, query, timeFieldName, earliestMs, latestMs } = payload; - return calculateModelMemoryLimitProvider(legacyClient)( + return calculateModelMemoryLimitProvider(client)( analysisConfig as AnalysisConfig, indexPattern, query, @@ -61,10 +61,10 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { let errorResp; - const resp = await estimateBucketSpanFactory(legacyClient)(request.body) + const resp = await estimateBucketSpanFactory(client)(request.body) // this catch gets triggered when the estimation code runs without error // but isn't able to come up with a bucket span estimation. // this doesn't return a HTTP error but an object with an error message @@ -109,9 +109,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await calculateModelMemoryLimit(legacyClient, request.body); + const resp = await calculateModelMemoryLimit(client, request.body); return response.ok({ body: resp, @@ -141,9 +141,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await validateCardinality(legacyClient, request.body); + const resp = await validateCardinality(client, request.body); return response.ok({ body: resp, @@ -173,11 +173,11 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { // version corresponds to the version used in documentation links. const resp = await validateJob( - legacyClient, + client, request.body, version, mlLicense.isSecurityEnabled() === false diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index 23e37d2213029..72a4c5e428c2b 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -6,11 +6,7 @@ import { TypeOf } from '@kbn/config-schema'; -import { - ILegacyScopedClusterClient, - KibanaRequest, - SavedObjectsClientContract, -} from 'kibana/server'; +import { IScopedClusterClient, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import { DatafeedOverride, JobOverride } from '../../common/types/modules'; import { wrapError } from '../client/error_wrapper'; import { DataRecognizer } from '../models/data_recognizer'; @@ -23,22 +19,22 @@ import { import { RouteInitialization } from '../types'; function recognize( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract, request: KibanaRequest, indexPatternTitle: string ) { - const dr = new DataRecognizer(legacyClient, savedObjectsClient, request); + const dr = new DataRecognizer(client, savedObjectsClient, request); return dr.findMatches(indexPatternTitle); } function getModule( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract, request: KibanaRequest, moduleId: string ) { - const dr = new DataRecognizer(legacyClient, savedObjectsClient, request); + const dr = new DataRecognizer(client, savedObjectsClient, request); if (moduleId === undefined) { return dr.listModules(); } else { @@ -47,7 +43,7 @@ function getModule( } function setup( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract, request: KibanaRequest, moduleId: string, @@ -63,7 +59,7 @@ function setup( datafeedOverrides?: DatafeedOverride | DatafeedOverride[], estimateModelMemory?: boolean ) { - const dr = new DataRecognizer(legacyClient, savedObjectsClient, request); + const dr = new DataRecognizer(client, savedObjectsClient, request); return dr.setup( moduleId, prefix, @@ -81,12 +77,12 @@ function setup( } function dataRecognizerJobsExist( - legacyClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract, request: KibanaRequest, moduleId: string ) { - const dr = new DataRecognizer(legacyClient, savedObjectsClient, request); + const dr = new DataRecognizer(client, savedObjectsClient, request); return dr.dataRecognizerJobsExist(moduleId); } @@ -131,11 +127,11 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response, context }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response, context }) => { try { const { indexPatternTitle } = request.params; const results = await recognize( - legacyClient, + client, context.core.savedObjects.client, request, indexPatternTitle @@ -266,7 +262,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response, context }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response, context }) => { try { let { moduleId } = request.params; if (moduleId === '') { @@ -275,7 +271,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { moduleId = undefined; } const results = await getModule( - legacyClient, + client, context.core.savedObjects.client, request, moduleId @@ -439,7 +435,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canCreateJob'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response, context }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response, context }) => { try { const { moduleId } = request.params; @@ -458,7 +454,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { } = request.body as TypeOf; const result = await setup( - legacyClient, + client, context.core.savedObjects.client, request, moduleId, @@ -544,11 +540,11 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response, context }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response, context }) => { try { const { moduleId } = request.params; const result = await dataRecognizerJobsExist( - legacyClient, + client, context.core.savedObjects.client, request, moduleId diff --git a/x-pack/plugins/ml/server/routes/notification_settings.ts b/x-pack/plugins/ml/server/routes/notification_settings.ts index 09c145d6257a8..5bb51bf9e596c 100644 --- a/x-pack/plugins/ml/server/routes/notification_settings.ts +++ b/x-pack/plugins/ml/server/routes/notification_settings.ts @@ -26,16 +26,15 @@ export function notificationRoutes({ router, mlLicense }: RouteInitialization) { tags: ['access:ml:canAccessML'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, response }) => { try { - const params = { - includeDefaults: true, - filterPath: '**.xpack.notification', - }; - const resp = await legacyClient.callAsCurrentUser('cluster.getSettings', params); + const { body } = await client.asCurrentUser.cluster.getSettings({ + include_defaults: true, + filter_path: '**.xpack.notification', + }); return response.ok({ - body: resp, + body, }); } catch (e) { return response.customError(wrapError(e)); diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 2af37c17f714a..4e34320d51333 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; @@ -23,8 +23,8 @@ import { getCategorizerStoppedPartitionsSchema, } from './schemas/results_service_schema'; -function getAnomaliesTableData(legacyClient: ILegacyScopedClusterClient, payload: any) { - const rs = resultsServiceProvider(legacyClient); +function getAnomaliesTableData(client: IScopedClusterClient, payload: any) { + const rs = resultsServiceProvider(client); const { jobIds, criteriaFields, @@ -53,39 +53,39 @@ function getAnomaliesTableData(legacyClient: ILegacyScopedClusterClient, payload ); } -function getCategoryDefinition(legacyClient: ILegacyScopedClusterClient, payload: any) { - const rs = resultsServiceProvider(legacyClient); +function getCategoryDefinition(client: IScopedClusterClient, payload: any) { + const rs = resultsServiceProvider(client); return rs.getCategoryDefinition(payload.jobId, payload.categoryId); } -function getCategoryExamples(legacyClient: ILegacyScopedClusterClient, payload: any) { - const rs = resultsServiceProvider(legacyClient); +function getCategoryExamples(client: IScopedClusterClient, payload: any) { + const rs = resultsServiceProvider(client); const { jobId, categoryIds, maxExamples } = payload; return rs.getCategoryExamples(jobId, categoryIds, maxExamples); } -function getMaxAnomalyScore(legacyClient: ILegacyScopedClusterClient, payload: any) { - const rs = resultsServiceProvider(legacyClient); +function getMaxAnomalyScore(client: IScopedClusterClient, payload: any) { + const rs = resultsServiceProvider(client); const { jobIds, earliestMs, latestMs } = payload; return rs.getMaxAnomalyScore(jobIds, earliestMs, latestMs); } -function getPartitionFieldsValues(legacyClient: ILegacyScopedClusterClient, payload: any) { - const rs = resultsServiceProvider(legacyClient); +function getPartitionFieldsValues(client: IScopedClusterClient, payload: any) { + const rs = resultsServiceProvider(client); const { jobId, searchTerm, criteriaFields, earliestMs, latestMs } = payload; return rs.getPartitionFieldsValues(jobId, searchTerm, criteriaFields, earliestMs, latestMs); } -function getCategorizerStats(legacyClient: ILegacyScopedClusterClient, params: any, query: any) { +function getCategorizerStats(client: IScopedClusterClient, params: any, query: any) { const { jobId } = params; const { partitionByValue } = query; - const rs = resultsServiceProvider(legacyClient); + const rs = resultsServiceProvider(client); return rs.getCategorizerStats(jobId, partitionByValue); } -function getCategoryStoppedPartitions(legacyClient: ILegacyScopedClusterClient, payload: any) { +function getCategoryStoppedPartitions(client: IScopedClusterClient, payload: any) { const { jobIds, fieldToBucket } = payload; - const rs = resultsServiceProvider(legacyClient); + const rs = resultsServiceProvider(client); return rs.getCategoryStoppedPartitions(jobIds, fieldToBucket); } @@ -112,9 +112,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getAnomaliesTableData(legacyClient, request.body); + const resp = await getAnomaliesTableData(client, request.body); return response.ok({ body: resp, @@ -144,9 +144,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getCategoryDefinition(legacyClient, request.body); + const resp = await getCategoryDefinition(client, request.body); return response.ok({ body: resp, @@ -176,9 +176,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getMaxAnomalyScore(legacyClient, request.body); + const resp = await getMaxAnomalyScore(client, request.body); return response.ok({ body: resp, @@ -208,9 +208,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getCategoryExamples(legacyClient, request.body); + const resp = await getCategoryExamples(client, request.body); return response.ok({ body: resp, @@ -240,9 +240,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getPartitionFieldsValues(legacyClient, request.body); + const resp = await getPartitionFieldsValues(client, request.body); return response.ok({ body: resp, @@ -269,14 +269,14 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { - const body = { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { + const { body } = await client.asInternalUser.search({ ...request.body, index: ML_RESULTS_INDEX_PATTERN, - }; + }); try { return response.ok({ - body: await legacyClient.callAsInternalUser('search', body), + body, }); } catch (error) { return response.customError(wrapError(error)); @@ -304,9 +304,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getCategorizerStats(legacyClient, request.params, request.query); + const resp = await getCategorizerStats(client, request.params, request.query); return response.ok({ body: resp, }); @@ -334,9 +334,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { - const resp = await getCategoryStoppedPartitions(legacyClient, request.body); + const resp = await getCategoryStoppedPartitions(client, request.body); return response.ok({ body: resp, }); diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index 273b86163245f..3a66f60943bb3 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { Request } from 'hapi'; -import { ILegacyScopedClusterClient } from 'kibana/server'; +import { IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { mlLog } from '../client/log'; import { capabilitiesProvider } from '../lib/capabilities'; @@ -21,17 +21,16 @@ export function systemRoutes( { router, mlLicense }: RouteInitialization, { spaces, cloud, resolveMlCapabilities }: SystemRouteDeps ) { - async function getNodeCount(legacyClient: ILegacyScopedClusterClient) { - const filterPath = 'nodes.*.attributes'; - const resp = await legacyClient.callAsInternalUser('nodes.info', { - filterPath, + async function getNodeCount(client: IScopedClusterClient) { + const { body } = await client.asInternalUser.nodes.info({ + filter_path: 'nodes.*.attributes', }); let count = 0; - if (typeof resp.nodes === 'object') { - Object.keys(resp.nodes).forEach((k) => { - if (resp.nodes[k].attributes !== undefined) { - const maxOpenJobs = resp.nodes[k].attributes['ml.max_open_jobs']; + if (typeof body.nodes === 'object') { + Object.keys(body.nodes).forEach((k) => { + if (body.nodes[k].attributes !== undefined) { + const maxOpenJobs = body.nodes[k].attributes['ml.max_open_jobs']; if (maxOpenJobs !== null && maxOpenJobs > 0) { count++; } @@ -58,15 +57,15 @@ export function systemRoutes( tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { - const { callAsCurrentUser, callAsInternalUser } = legacyClient; + const { asCurrentUser, asInternalUser } = client; let upgradeInProgress = false; try { - const info = await callAsInternalUser('ml.info'); + const { body } = await asInternalUser.ml.info(); // if ml indices are currently being migrated, upgrade_mode will be set to true // pass this back with the privileges to allow for the disabling of UI controls. - upgradeInProgress = info.upgrade_mode === true; + upgradeInProgress = body.upgrade_mode === true; } catch (error) { // if the ml.info check fails, it could be due to the user having insufficient privileges // most likely they do not have the ml_user role and therefore will be blocked from using @@ -90,11 +89,12 @@ export function systemRoutes( }, }); } else { - const body = request.body; - const resp = await callAsCurrentUser('ml.privilegeCheck', { body }); - resp.upgradeInProgress = upgradeInProgress; + const { body } = await asCurrentUser.security.hasPrivileges({ body: request.body }); return response.ok({ - body: resp, + body: { + ...body, + upgradeInProgress, + }, }); } } catch (error) { @@ -115,7 +115,7 @@ export function systemRoutes( path: '/api/ml/ml_capabilities', validate: false, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { // if spaces is disabled force isMlEnabledInSpace to be true const { isMlEnabledInSpace } = @@ -129,7 +129,7 @@ export function systemRoutes( } const { getCapabilities } = capabilitiesProvider( - legacyClient, + client, mlCapabilities, mlLicense, isMlEnabledInSpace @@ -159,10 +159,10 @@ export function systemRoutes( }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { return response.ok({ - body: await getNodeCount(legacyClient), + body: await getNodeCount(client), }); } catch (e) { return response.customError(wrapError(e)); @@ -185,12 +185,12 @@ export function systemRoutes( tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { - const info = await legacyClient.callAsInternalUser('ml.info'); + const { body } = await client.asInternalUser.ml.info(); const cloudId = cloud && cloud.cloudId; return response.ok({ - body: { ...info, cloudId }, + body: { ...body, cloudId }, }); } catch (error) { return response.customError(wrapError(error)); @@ -216,10 +216,11 @@ export function systemRoutes( tags: ['access:ml:canGetJobs'], }, }, - mlLicense.fullLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { try { + const { body } = await client.asCurrentUser.search(request.body); return response.ok({ - body: await legacyClient.callAsCurrentUser('search', request.body), + body, }); } catch (error) { return response.customError(wrapError(error)); @@ -243,22 +244,21 @@ export function systemRoutes( tags: ['access:ml:canAccessML'], }, }, - mlLicense.basicLicenseAPIGuard(async ({ legacyClient, request, response }) => { + mlLicense.basicLicenseAPIGuard(async ({ client, request, response }) => { try { const { index } = request.body; const options = { index: [index], fields: ['*'], - ignoreUnavailable: true, - allowNoIndices: true, - ignore: 404, + ignore_unavailable: true, + allow_no_indices: true, }; - const fieldsResult = await legacyClient.callAsCurrentUser('fieldCaps', options); + const { body } = await client.asCurrentUser.fieldCaps(options); const result = { exists: false }; - if (Array.isArray(fieldsResult.indices) && fieldsResult.indices.length !== 0) { + if (Array.isArray(body.indices) && body.indices.length !== 0) { result.exists = true; } diff --git a/x-pack/plugins/ml/server/shared_services/errors.ts b/x-pack/plugins/ml/server/shared_services/errors.ts new file mode 100644 index 0000000000000..f15a85a490a46 --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/errors.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class MLClusterClientUninitialized extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/x-pack/plugins/ml/server/shared_services/license_checks/errors.ts b/x-pack/plugins/ml/server/shared_services/license_checks/errors.ts new file mode 100644 index 0000000000000..18e7dab43fda7 --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/license_checks/errors.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/* eslint-disable max-classes-per-file */ + +export class InsufficientFullLicenseError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class InsufficientBasicLicenseError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/x-pack/plugins/ml/server/shared_services/license_checks/index.ts b/x-pack/plugins/ml/server/shared_services/license_checks/index.ts new file mode 100644 index 0000000000000..6b837dadf5c0d --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/license_checks/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { LicenseCheck, licenseChecks } from './license_checks'; +export { InsufficientBasicLicenseError, InsufficientFullLicenseError } from './errors'; diff --git a/x-pack/plugins/ml/server/shared_services/license_checks.ts b/x-pack/plugins/ml/server/shared_services/license_checks/license_checks.ts similarity index 66% rename from x-pack/plugins/ml/server/shared_services/license_checks.ts rename to x-pack/plugins/ml/server/shared_services/license_checks/license_checks.ts index 191124ffa5f3a..3d9de1ef70f2d 100644 --- a/x-pack/plugins/ml/server/shared_services/license_checks.ts +++ b/x-pack/plugins/ml/server/shared_services/license_checks/license_checks.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MlServerLicense } from '../lib/license'; +import { MlServerLicense } from '../../lib/license'; +import { InsufficientFullLicenseError, InsufficientBasicLicenseError } from './errors'; export type LicenseCheck = () => void; @@ -14,12 +15,12 @@ export function licenseChecks( return { isFullLicense() { if (mlLicense.isFullLicense() === false) { - throw Error('Platinum, Enterprise or trial license needed'); + throw new InsufficientFullLicenseError('Platinum, Enterprise or trial license needed'); } }, isMinimumLicense() { if (mlLicense.isMinimumLicense() === false) { - throw Error('Basic license needed'); + throw new InsufficientBasicLicenseError('Basic license needed'); } }, }; diff --git a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts index 603b4fba17adb..53898cb64d07f 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts @@ -4,40 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient, KibanaRequest } from 'kibana/server'; +import { KibanaRequest } from 'kibana/server'; import { Job } from '../../../common/types/anomaly_detection_jobs'; -import { SharedServicesChecks } from '../shared_services'; +import { GetGuards } from '../shared_services'; export interface AnomalyDetectorsProvider { anomalyDetectorsProvider( - mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest ): { jobs(jobId?: string): Promise<{ count: number; jobs: Job[] }>; }; } -export function getAnomalyDetectorsProvider({ - isFullLicense, - getHasMlCapabilities, -}: SharedServicesChecks): AnomalyDetectorsProvider { +export function getAnomalyDetectorsProvider(getGuards: GetGuards): AnomalyDetectorsProvider { return { - anomalyDetectorsProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) { - // APM is using this service in anomaly alert, kibana alerting doesn't provide request object - // So we are adding a dummy request for now - // TODO: Remove this once kibana alerting provides request object - const hasMlCapabilities = - request.params !== 'DummyKibanaRequest' - ? getHasMlCapabilities(request) - : (_caps: string[]) => Promise.resolve(); + anomalyDetectorsProvider(request: KibanaRequest) { return { async jobs(jobId?: string) { - isFullLicense(); - await hasMlCapabilities(['canGetJobs']); - return mlClusterClient.callAsInternalUser( - 'ml.jobs', - jobId !== undefined ? { jobId } : {} - ); + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ scopedClient }) => { + const { body } = await scopedClient.asInternalUser.ml.getJobs<{ + count: number; + jobs: Job[]; + }>(jobId !== undefined ? { job_id: jobId } : undefined); + return body; + }); }, }; }, diff --git a/x-pack/plugins/ml/server/shared_services/providers/job_service.ts b/x-pack/plugins/ml/server/shared_services/providers/job_service.ts index c734dcc1583a1..2897bf08717f8 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/job_service.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/job_service.ts @@ -4,38 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient, KibanaRequest } from 'kibana/server'; +import { KibanaRequest } from 'kibana/server'; import { jobServiceProvider } from '../../models/job_service'; -import { SharedServicesChecks } from '../shared_services'; +import { GetGuards } from '../shared_services'; type OrigJobServiceProvider = ReturnType; export interface JobServiceProvider { jobServiceProvider( - mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest ): { jobsSummary: OrigJobServiceProvider['jobsSummary']; }; } -export function getJobServiceProvider({ - isFullLicense, - getHasMlCapabilities, -}: SharedServicesChecks): JobServiceProvider { +export function getJobServiceProvider(getGuards: GetGuards): JobServiceProvider { return { - jobServiceProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) { - // const hasMlCapabilities = getHasMlCapabilities(request); - const { jobsSummary } = jobServiceProvider(mlClusterClient); + jobServiceProvider(request: KibanaRequest) { return { - async jobsSummary(...args) { - isFullLicense(); - // Removed while https://github.com/elastic/kibana/issues/64588 exists. - // SIEM are calling this endpoint with a dummy request object from their alerting - // integration and currently alerting does not supply a request object. - // await hasMlCapabilities(['canGetJobs']); - - return jobsSummary(...args); + jobsSummary: async (...args) => { + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ scopedClient }) => { + const { jobsSummary } = jobServiceProvider(scopedClient); + return jobsSummary(...args); + }); }, }; }, diff --git a/x-pack/plugins/ml/server/shared_services/providers/modules.ts b/x-pack/plugins/ml/server/shared_services/providers/modules.ts index fb7d59f9c8218..a727d96433f1d 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/modules.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/modules.ts @@ -4,23 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ILegacyScopedClusterClient, - KibanaRequest, - SavedObjectsClientContract, -} from 'kibana/server'; +import { IScopedClusterClient, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { DataRecognizer } from '../../models/data_recognizer'; -import { SharedServicesChecks } from '../shared_services'; +import { GetGuards } from '../shared_services'; import { moduleIdParamSchema, setupModuleBodySchema } from '../../routes/schemas/modules'; -import { HasMlCapabilities } from '../../lib/capabilities'; export type ModuleSetupPayload = TypeOf & TypeOf; export interface ModulesProvider { modulesProvider( - mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest, savedObjectsClient: SavedObjectsClientContract ): { @@ -31,61 +25,58 @@ export interface ModulesProvider { }; } -export function getModulesProvider({ - isFullLicense, - getHasMlCapabilities, -}: SharedServicesChecks): ModulesProvider { +export function getModulesProvider(getGuards: GetGuards): ModulesProvider { return { - modulesProvider( - mlClusterClient: ILegacyScopedClusterClient, - request: KibanaRequest, - savedObjectsClient: SavedObjectsClientContract - ) { - let hasMlCapabilities: HasMlCapabilities; - if (request.params === 'DummyKibanaRequest') { - hasMlCapabilities = () => Promise.resolve(); - } else { - hasMlCapabilities = getHasMlCapabilities(request); - } - const dr = dataRecognizerFactory(mlClusterClient, savedObjectsClient, request); - + modulesProvider(request: KibanaRequest, savedObjectsClient: SavedObjectsClientContract) { return { async recognize(...args) { - isFullLicense(); - await hasMlCapabilities(['canCreateJob']); - - return dr.findMatches(...args); + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ scopedClient }) => { + const dr = dataRecognizerFactory(scopedClient, savedObjectsClient, request); + return dr.findMatches(...args); + }); }, async getModule(moduleId: string) { - isFullLicense(); - await hasMlCapabilities(['canGetJobs']); - - return dr.getModule(moduleId); + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ scopedClient }) => { + const dr = dataRecognizerFactory(scopedClient, savedObjectsClient, request); + return dr.getModule(moduleId); + }); }, async listModules() { - isFullLicense(); - await hasMlCapabilities(['canGetJobs']); - - return dr.listModules(); + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ scopedClient }) => { + const dr = dataRecognizerFactory(scopedClient, savedObjectsClient, request); + return dr.listModules(); + }); }, async setup(payload: ModuleSetupPayload) { - isFullLicense(); - await hasMlCapabilities(['canCreateJob']); - - return dr.setup( - payload.moduleId, - payload.prefix, - payload.groups, - payload.indexPatternName, - payload.query, - payload.useDedicatedIndex, - payload.startDatafeed, - payload.start, - payload.end, - payload.jobOverrides, - payload.datafeedOverrides, - payload.estimateModelMemory - ); + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canCreateJob']) + .ok(async ({ scopedClient }) => { + const dr = dataRecognizerFactory(scopedClient, savedObjectsClient, request); + return dr.setup( + payload.moduleId, + payload.prefix, + payload.groups, + payload.indexPatternName, + payload.query, + payload.useDedicatedIndex, + payload.startDatafeed, + payload.start, + payload.end, + payload.jobOverrides, + payload.datafeedOverrides, + payload.estimateModelMemory + ); + }); }, }; }, @@ -93,9 +84,9 @@ export function getModulesProvider({ } function dataRecognizerFactory( - mlClusterClient: ILegacyScopedClusterClient, + client: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract, request: KibanaRequest ) { - return new DataRecognizer(mlClusterClient, savedObjectsClient, request); + return new DataRecognizer(client, savedObjectsClient, request); } diff --git a/x-pack/plugins/ml/server/shared_services/providers/results_service.ts b/x-pack/plugins/ml/server/shared_services/providers/results_service.ts index 6af4eb008567a..5536765cc376a 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/results_service.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/results_service.ts @@ -4,41 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient, KibanaRequest } from 'kibana/server'; +import { KibanaRequest } from 'kibana/server'; import { resultsServiceProvider } from '../../models/results_service'; -import { SharedServicesChecks } from '../shared_services'; +import { GetGuards } from '../shared_services'; type OrigResultsServiceProvider = ReturnType; export interface ResultsServiceProvider { resultsServiceProvider( - mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest ): { getAnomaliesTableData: OrigResultsServiceProvider['getAnomaliesTableData']; }; } -export function getResultsServiceProvider({ - isFullLicense, - getHasMlCapabilities, -}: SharedServicesChecks): ResultsServiceProvider { +export function getResultsServiceProvider(getGuards: GetGuards): ResultsServiceProvider { return { - resultsServiceProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) { - // Uptime is using this service in anomaly alert, kibana alerting doesn't provide request object - // So we are adding a dummy request for now - // TODO: Remove this once kibana alerting provides request object - const hasMlCapabilities = - request.params !== 'DummyKibanaRequest' - ? getHasMlCapabilities(request) - : (_caps: string[]) => Promise.resolve(); - - const { getAnomaliesTableData } = resultsServiceProvider(mlClusterClient); + resultsServiceProvider(request: KibanaRequest) { return { async getAnomaliesTableData(...args) { - isFullLicense(); - await hasMlCapabilities(['canGetJobs']); - return getAnomaliesTableData(...args); + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ scopedClient }) => { + const { getAnomaliesTableData } = resultsServiceProvider(scopedClient); + return getAnomaliesTableData(...args); + }); }, }; }, diff --git a/x-pack/plugins/ml/server/shared_services/providers/system.ts b/x-pack/plugins/ml/server/shared_services/providers/system.ts index d292abc438a2f..3217ff13787b0 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/system.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/system.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient, KibanaRequest } from 'kibana/server'; -import { SearchResponse, SearchParams } from 'elasticsearch'; +import { KibanaRequest } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; +import { RequestParams } from '@elastic/elasticsearch'; import { MlServerLicense } from '../../lib/license'; import { CloudSetup } from '../../../../cloud/server'; import { spacesUtilsProvider } from '../../lib/spaces_utils'; @@ -14,73 +15,79 @@ import { capabilitiesProvider } from '../../lib/capabilities'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { MlCapabilitiesResponse, ResolveMlCapabilities } from '../../../common/types/capabilities'; -import { SharedServicesChecks } from '../shared_services'; +import { GetGuards } from '../shared_services'; export interface MlSystemProvider { mlSystemProvider( - mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest ): { mlCapabilities(): Promise; mlInfo(): Promise; - mlAnomalySearch(searchParams: SearchParams): Promise>; + mlAnomalySearch(searchParams: RequestParams.Search): Promise>; }; } export function getMlSystemProvider( - { isMinimumLicense, isFullLicense, getHasMlCapabilities }: SharedServicesChecks, + getGuards: GetGuards, mlLicense: MlServerLicense, spaces: SpacesPluginSetup | undefined, cloud: CloudSetup | undefined, resolveMlCapabilities: ResolveMlCapabilities ): MlSystemProvider { return { - mlSystemProvider(mlClusterClient: ILegacyScopedClusterClient, request: KibanaRequest) { - // const hasMlCapabilities = getHasMlCapabilities(request); - const { callAsInternalUser } = mlClusterClient; + mlSystemProvider(request: KibanaRequest) { return { async mlCapabilities() { - isMinimumLicense(); + return await getGuards(request) + .isMinimumLicense() + .ok(async ({ scopedClient }) => { + const { isMlEnabledInSpace } = + spaces !== undefined + ? spacesUtilsProvider(spaces, request) + : { isMlEnabledInSpace: async () => true }; - const { isMlEnabledInSpace } = - spaces !== undefined - ? spacesUtilsProvider(spaces, request) - : { isMlEnabledInSpace: async () => true }; + const mlCapabilities = await resolveMlCapabilities(request); + if (mlCapabilities === null) { + throw new Error('mlCapabilities is not defined'); + } - const mlCapabilities = await resolveMlCapabilities(request); - if (mlCapabilities === null) { - throw new Error('mlCapabilities is not defined'); - } - - const { getCapabilities } = capabilitiesProvider( - mlClusterClient, - mlCapabilities, - mlLicense, - isMlEnabledInSpace - ); - return getCapabilities(); + const { getCapabilities } = capabilitiesProvider( + scopedClient, + mlCapabilities, + mlLicense, + isMlEnabledInSpace + ); + return getCapabilities(); + }); }, async mlInfo(): Promise { - isMinimumLicense(); + return await getGuards(request) + .isMinimumLicense() + .ok(async ({ scopedClient }) => { + const { asInternalUser } = scopedClient; - const info = await callAsInternalUser('ml.info'); - const cloudId = cloud && cloud.cloudId; - return { - ...info, - cloudId, - }; + const { body: info } = await asInternalUser.ml.info(); + const cloudId = cloud && cloud.cloudId; + return { + ...info, + cloudId, + }; + }); }, - async mlAnomalySearch(searchParams: SearchParams): Promise> { - isFullLicense(); - // Removed while https://github.com/elastic/kibana/issues/64588 exists. - // SIEM are calling this endpoint with a dummy request object from their alerting - // integration and currently alerting does not supply a request object. - // await hasMlCapabilities(['canAccessML']); - - return callAsInternalUser('search', { - ...searchParams, - index: ML_RESULTS_INDEX_PATTERN, - }); + async mlAnomalySearch( + searchParams: RequestParams.Search + ): Promise> { + return await getGuards(request) + .isFullLicense() + .hasMlCapabilities(['canAccessML']) + .ok(async ({ scopedClient }) => { + const { asInternalUser } = scopedClient; + const { body } = await asInternalUser.search>({ + ...searchParams, + index: ML_RESULTS_INDEX_PATTERN, + }); + return body; + }); }, }; }, diff --git a/x-pack/plugins/ml/server/shared_services/shared_services.ts b/x-pack/plugins/ml/server/shared_services/shared_services.ts index 3345111fad4ae..4c568e4515a27 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'kibana/server'; +import { IClusterClient, IScopedClusterClient } from 'kibana/server'; +// including KibanaRequest from 'kibana/server' causes an error +// when being used with instanceof +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaRequest } from '../../.././../../src/core/server/http'; import { MlServerLicense } from '../lib/license'; import { SpacesPluginSetup } from '../../../spaces/server'; @@ -18,8 +22,9 @@ import { AnomalyDetectorsProvider, getAnomalyDetectorsProvider, } from './providers/anomaly_detectors'; -import { ResolveMlCapabilities } from '../../common/types/capabilities'; +import { ResolveMlCapabilities, MlCapabilitiesKey } from '../../common/types/capabilities'; import { hasMlCapabilitiesProvider, HasMlCapabilities } from '../lib/capabilities'; +import { MLClusterClientUninitialized } from './errors'; export type SharedServices = JobServiceProvider & AnomalyDetectorsProvider & @@ -27,31 +32,97 @@ export type SharedServices = JobServiceProvider & ModulesProvider & ResultsServiceProvider; +interface Guards { + isMinimumLicense(): Guards; + isFullLicense(): Guards; + hasMlCapabilities: (caps: MlCapabilitiesKey[]) => Guards; + ok(callback: OkCallback): any; +} + +export type GetGuards = (request: KibanaRequest) => Guards; + export interface SharedServicesChecks { - isFullLicense(): void; - isMinimumLicense(): void; - getHasMlCapabilities(request: KibanaRequest): HasMlCapabilities; + getGuards(request: KibanaRequest): Guards; } +interface OkParams { + scopedClient: IScopedClusterClient; +} + +type OkCallback = (okParams: OkParams) => any; + export function createSharedServices( mlLicense: MlServerLicense, spaces: SpacesPluginSetup | undefined, cloud: CloudSetup, - resolveMlCapabilities: ResolveMlCapabilities + resolveMlCapabilities: ResolveMlCapabilities, + getClusterClient: () => IClusterClient | null ): SharedServices { + const getRequestItems = getRequestItemsProvider(resolveMlCapabilities, getClusterClient); const { isFullLicense, isMinimumLicense } = licenseChecks(mlLicense); - const getHasMlCapabilities = hasMlCapabilitiesProvider(resolveMlCapabilities); - const checks: SharedServicesChecks = { - isFullLicense, - isMinimumLicense, - getHasMlCapabilities, - }; + + function getGuards(request: KibanaRequest): Guards { + const { hasMlCapabilities, scopedClient } = getRequestItems(request); + const asyncGuards: Array> = []; + + const guards: Guards = { + isMinimumLicense: () => { + isMinimumLicense(); + return guards; + }, + isFullLicense: () => { + isFullLicense(); + return guards; + }, + hasMlCapabilities: (caps: MlCapabilitiesKey[]) => { + asyncGuards.push(hasMlCapabilities(caps)); + return guards; + }, + async ok(callback: OkCallback) { + await Promise.all(asyncGuards); + return callback({ scopedClient }); + }, + }; + return guards; + } return { - ...getJobServiceProvider(checks), - ...getAnomalyDetectorsProvider(checks), - ...getModulesProvider(checks), - ...getResultsServiceProvider(checks), - ...getMlSystemProvider(checks, mlLicense, spaces, cloud, resolveMlCapabilities), + ...getJobServiceProvider(getGuards), + ...getAnomalyDetectorsProvider(getGuards), + ...getModulesProvider(getGuards), + ...getResultsServiceProvider(getGuards), + ...getMlSystemProvider(getGuards, mlLicense, spaces, cloud, resolveMlCapabilities), + }; +} + +function getRequestItemsProvider( + resolveMlCapabilities: ResolveMlCapabilities, + getClusterClient: () => IClusterClient | null +) { + return (request: KibanaRequest) => { + const getHasMlCapabilities = hasMlCapabilitiesProvider(resolveMlCapabilities); + let hasMlCapabilities: HasMlCapabilities; + let scopedClient: IScopedClusterClient; + // While https://github.com/elastic/kibana/issues/64588 exists we + // will not receive a real request object when being called from an alert. + // instead a dummy request object will be supplied + const clusterClient = getClusterClient(); + + if (clusterClient === null) { + throw new MLClusterClientUninitialized(`ML's cluster client has not been initialized`); + } + + if (request instanceof KibanaRequest) { + hasMlCapabilities = getHasMlCapabilities(request); + scopedClient = clusterClient.asScoped(request); + } else { + hasMlCapabilities = () => Promise.resolve(); + const { asInternalUser } = clusterClient; + scopedClient = { + asInternalUser, + asCurrentUser: asInternalUser, + }; + } + return { hasMlCapabilities, scopedClient }; }; } diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json deleted file mode 100644 index 4082f16a5d91c..0000000000000 --- a/x-pack/plugins/ml/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../tsconfig.json" -} diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index 5f23bbc390bb8..0a76c7fcfd3b2 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -12,7 +12,8 @@ import { parse as parseUrl } from 'url'; import { getDisallowedOutgoingUrlError } from '../'; import { LevelLogger } from '../../../lib'; import { ViewZoomWidthHeight } from '../../../lib/layouts/layout'; -import { ConditionalHeaders, ElementPosition } from '../../../types'; +import { ElementPosition } from '../../../lib/screenshots'; +import { ConditionalHeaders } from '../../../types'; import { allowRequest, NetworkPolicy } from '../../network_policy'; export interface ChromiumDriverOptions { diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index 25594e1c0140b..be32b52f19813 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -17,12 +17,11 @@ import { } from 'src/core/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; -import { ScreenshotsObservableFn } from '../server/types'; import { ReportingConfig } from './'; import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; -import { screenshotsObservableFactory } from './lib/screenshots'; import { checkLicense, getExportTypesRegistry } from './lib'; import { ESQueueInstance } from './lib/create_queue'; +import { screenshotsObservableFactory, ScreenshotsObservableFn } from './lib/screenshots'; import { ReportingStore } from './lib/store'; export interface ReportingInternalSetup { diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts index 845b9adb38be9..5ab029bfd9f29 100644 --- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts @@ -14,14 +14,14 @@ interface HasEncryptedHeaders { // TODO merge functionality with CSV execute job export const decryptJobHeaders = async < JobParamsType, - ScheduledTaskParamsType extends HasEncryptedHeaders + TaskPayloadType extends HasEncryptedHeaders >({ encryptionKey, job, logger, }: { encryptionKey?: string; - job: ScheduledTaskParamsType; + job: TaskPayloadType; logger: LevelLogger; }): Promise> => { try { diff --git a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts index 0372d515c21a8..754bc7bc75cb5 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts @@ -8,8 +8,8 @@ import sinon from 'sinon'; import { ReportingConfig } from '../../'; import { ReportingCore } from '../../core'; import { createMockReportingCore } from '../../test_helpers'; -import { ScheduledTaskParams } from '../../types'; -import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { BasePayload } from '../../types'; +import { TaskPayloadPDF } from '../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './'; let mockConfig: ReportingConfig; @@ -37,7 +37,7 @@ describe('conditions', () => { }; const conditionalHeaders = await getConditionalHeaders({ - job: {} as ScheduledTaskParams, + job: {} as BasePayload, filteredHeaders: permittedHeaders, config: mockConfig, }); @@ -64,14 +64,14 @@ test('uses basePath from job when creating saved object service', async () => { baz: 'quix', }; const conditionalHeaders = await getConditionalHeaders({ - job: {} as ScheduledTaskParams, + job: {} as BasePayload, filteredHeaders: permittedHeaders, config: mockConfig, }); const jobBasePath = '/sbp/s/marketing'; await getCustomLogo({ reporting: mockReportingPlugin, - job: { basePath: jobBasePath } as ScheduledTaskParamsPDF, + job: { basePath: jobBasePath } as TaskPayloadPDF, conditionalHeaders, config: mockConfig, }); @@ -94,14 +94,14 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav baz: 'quix', }; const conditionalHeaders = await getConditionalHeaders({ - job: {} as ScheduledTaskParams, + job: {} as BasePayload, filteredHeaders: permittedHeaders, config: mockConfig, }); await getCustomLogo({ reporting: mockReportingPlugin, - job: {} as ScheduledTaskParamsPDF, + job: {} as TaskPayloadPDF, conditionalHeaders, config: mockConfig, }); @@ -139,7 +139,7 @@ describe('config formatting', () => { mockConfig = getMockConfig(mockConfigGet); const conditionalHeaders = await getConditionalHeaders({ - job: {} as ScheduledTaskParams, + job: {} as BasePayload, filteredHeaders: {}, config: mockConfig, }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts index 799d023486832..ce83323914eb8 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts @@ -7,13 +7,13 @@ import { ReportingConfig } from '../../'; import { ConditionalHeaders } from '../../types'; -export const getConditionalHeaders = ({ +export const getConditionalHeaders = ({ config, job, filteredHeaders, }: { config: ReportingConfig; - job: ScheduledTaskParamsType; + job: TaskPayloadType; filteredHeaders: Record; }) => { const { kbnConfig } = config; diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts index a3d65a1398a20..8c02fdd69de8b 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts @@ -6,7 +6,7 @@ import { ReportingCore } from '../../core'; import { createMockReportingCore } from '../../test_helpers'; -import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { TaskPayloadPDF } from '../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './'; const mockConfigGet = jest.fn().mockImplementation((key: string) => { @@ -37,7 +37,7 @@ test(`gets logo from uiSettings`, async () => { }); const conditionalHeaders = await getConditionalHeaders({ - job: {} as ScheduledTaskParamsPDF, + job: {} as TaskPayloadPDF, filteredHeaders: permittedHeaders, config: mockConfig, }); @@ -45,7 +45,7 @@ test(`gets logo from uiSettings`, async () => { const { logo } = await getCustomLogo({ reporting: mockReportingPlugin, config: mockConfig, - job: {} as ScheduledTaskParamsPDF, + job: {} as TaskPayloadPDF, conditionalHeaders, }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts index 547cc45258dae..ee61d76c8a933 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts @@ -7,7 +7,7 @@ import { ReportingConfig, ReportingCore } from '../../'; import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants'; import { ConditionalHeaders } from '../../types'; -import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; // Logo is PDF only +import { TaskPayloadPDF } from '../printable_pdf/types'; // Logo is PDF only export const getCustomLogo = async ({ reporting, @@ -17,7 +17,7 @@ export const getCustomLogo = async ({ }: { reporting: ReportingCore; config: ReportingConfig; - job: ScheduledTaskParamsPDF; + job: TaskPayloadPDF; conditionalHeaders: ConditionalHeaders; }) => { const serverBasePath: string = config.kbnConfig.get('server', 'basePath'); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts index 73d7c7b03c128..355536000326e 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts @@ -5,12 +5,12 @@ */ import { ReportingConfig } from '../../'; -import { ScheduledTaskParamsPNG } from '../png/types'; -import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { TaskPayloadPNG } from '../png/types'; +import { TaskPayloadPDF } from '../printable_pdf/types'; import { getFullUrls } from './get_full_urls'; interface FullUrlsOpts { - job: ScheduledTaskParamsPNG & ScheduledTaskParamsPDF; + job: TaskPayloadPNG & TaskPayloadPDF; config: ReportingConfig; } @@ -35,7 +35,7 @@ beforeEach(() => { mockConfig = getMockConfig(mockConfigGet); }); -const getMockJob = (base: object) => base as ScheduledTaskParamsPNG & ScheduledTaskParamsPDF; +const getMockJob = (base: object) => base as TaskPayloadPNG & TaskPayloadPDF; test(`fails if no URL is passed`, async () => { const fn = () => getFullUrls({ job: getMockJob({}), config: mockConfig } as FullUrlsOpts); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts index d3362fd190680..d6f472e18bc7b 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts @@ -11,28 +11,24 @@ import { UrlWithStringQuery, } from 'url'; import { ReportingConfig } from '../../'; -import { ScheduledTaskParamsPNG } from '../png/types'; -import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { TaskPayloadPNG } from '../png/types'; +import { TaskPayloadPDF } from '../printable_pdf/types'; import { getAbsoluteUrlFactory } from './get_absolute_url'; import { validateUrls } from './validate_urls'; -function isPngJob( - job: ScheduledTaskParamsPNG | ScheduledTaskParamsPDF -): job is ScheduledTaskParamsPNG { - return (job as ScheduledTaskParamsPNG).relativeUrl !== undefined; +function isPngJob(job: TaskPayloadPNG | TaskPayloadPDF): job is TaskPayloadPNG { + return (job as TaskPayloadPNG).relativeUrl !== undefined; } -function isPdfJob( - job: ScheduledTaskParamsPNG | ScheduledTaskParamsPDF -): job is ScheduledTaskParamsPDF { - return (job as ScheduledTaskParamsPDF).relativeUrls !== undefined; +function isPdfJob(job: TaskPayloadPNG | TaskPayloadPDF): job is TaskPayloadPDF { + return (job as TaskPayloadPDF).relativeUrls !== undefined; } -export function getFullUrls({ +export function getFullUrls({ config, job, }: { config: ReportingConfig; - job: ScheduledTaskParamsPDF | ScheduledTaskParamsPNG; + job: TaskPayloadPDF | TaskPayloadPNG; }) { const [basePath, protocol, hostname, port] = [ config.kbnConfig.get('server', 'basePath'), diff --git a/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts b/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts index e56ffc737764c..b2e0ce23aa3a5 100644 --- a/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts @@ -9,11 +9,11 @@ import { KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN, } from '../../../common/constants'; -export const omitBlacklistedHeaders = ({ +export const omitBlacklistedHeaders = ({ job, decryptedHeaders, }: { - job: ScheduledTaskParamsType; + job: TaskPayloadType; decryptedHeaders: Record; }) => { const filteredHeaders: Record = omitBy( diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index 252968e386b53..be18bd7fff361 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -5,16 +5,16 @@ */ import { cryptoFactory } from '../../lib'; -import { CreateJobFn, ScheduleTaskFnFactory } from '../../types'; +import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { JobParamsDiscoverCsv } from './types'; -export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function scheduleTask(jobParams, context, request) { + return async function createJob(jobParams, context, request) { const serializedEncryptedHeaders = await crypto.encrypt(request.headers); const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts index 5eeef0f9906dd..15432d0cbd147 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts @@ -21,7 +21,7 @@ import { LevelLogger } from '../../lib'; import { setFieldFormats } from '../../services'; import { createMockReportingCore } from '../../test_helpers'; import { runTaskFnFactory } from './execute_job'; -import { ScheduledTaskParamsCSV } from './types'; +import { TaskPayloadCSV } from './types'; const delay = (ms: number) => new Promise((resolve) => setTimeout(() => resolve(), ms)); @@ -30,7 +30,7 @@ const getRandomScrollId = () => { return puid.generate(); }; -const getScheduledTaskParams = (baseObj: any) => baseObj as ScheduledTaskParamsCSV; +const getBasePayload = (baseObj: any) => baseObj as TaskPayloadCSV; describe('CSV Execute Job', function () { const encryptionKey = 'testEncryptionKey'; @@ -128,7 +128,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -146,7 +146,7 @@ describe('CSV Execute Job', function () { }; const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const job = getScheduledTaskParams({ + const job = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { @@ -175,7 +175,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -193,7 +193,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -227,7 +227,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -266,7 +266,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -296,7 +296,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: undefined, @@ -323,7 +323,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -348,7 +348,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], conflictedTypesFields: [], @@ -374,7 +374,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -400,7 +400,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], conflictedTypesFields: [], @@ -426,7 +426,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -453,7 +453,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -474,7 +474,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -497,7 +497,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -518,7 +518,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -534,7 +534,7 @@ describe('CSV Execute Job', function () { it('should reject Promise if search call errors out', async function () { callAsCurrentUserStub.rejects(new Error()); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -553,7 +553,7 @@ describe('CSV Execute Job', function () { }); callAsCurrentUserStub.onSecondCall().rejects(new Error()); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -574,7 +574,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -593,7 +593,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -619,7 +619,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -645,7 +645,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -681,7 +681,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); runTask( 'job345', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -700,7 +700,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); runTask( 'job345', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -718,7 +718,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); runTask( 'job345', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -738,7 +738,7 @@ describe('CSV Execute Job', function () { describe('csv content', function () { it('should write column headers to output, even if there are no results', async function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, @@ -750,7 +750,7 @@ describe('CSV Execute Job', function () { it('should use custom uiSettings csv:separator for header', async function () { mockUiSettingsClient.get.withArgs(CSV_SEPARATOR_SETTING).returns(';'); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, @@ -762,7 +762,7 @@ describe('CSV Execute Job', function () { it('should escape column headers if uiSettings csv:quoteValues is true', async function () { mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(true); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], searchRequest: { index: null, body: null }, @@ -774,7 +774,7 @@ describe('CSV Execute Job', function () { it(`shouldn't escape column headers if uiSettings csv:quoteValues is false`, async function () { mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(false); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], searchRequest: { index: null, body: null }, @@ -792,7 +792,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, @@ -813,7 +813,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -841,7 +841,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -864,7 +864,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -900,7 +900,7 @@ describe('CSV Execute Job', function () { configGetStub.withArgs('csv', 'maxSizeBytes').returns(1); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, @@ -930,7 +930,7 @@ describe('CSV Execute Job', function () { configGetStub.withArgs('csv', 'maxSizeBytes').returns(9); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, @@ -967,7 +967,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -1007,7 +1007,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -1044,7 +1044,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -1070,7 +1070,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -1096,7 +1096,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts index b56a08b86b0cd..b308935a29e92 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts @@ -10,9 +10,9 @@ import Hapi from 'hapi'; import { KibanaRequest } from '../../../../../../src/core/server'; import { CONTENT_TYPE_CSV, CSV_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory, LevelLogger } from '../../lib'; -import { WorkerExecuteFn, RunTaskFnFactory } from '../../types'; -import { ScheduledTaskParamsCSV } from './types'; +import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { createGenerateCsv } from './generate_csv'; +import { TaskPayloadCSV } from './types'; const getRequest = async (headers: string | undefined, crypto: Crypto, logger: LevelLogger) => { const decryptHeaders = async () => { @@ -55,8 +55,8 @@ const getRequest = async (headers: string | undefined, crypto: Crypto, logger: L } as Hapi.Request); }; -export const runTaskFnFactory: RunTaskFnFactory> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); diff --git a/x-pack/plugins/reporting/server/export_types/csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/index.ts index 4bca42e0661e5..b54844cdf1742 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/index.ts @@ -13,22 +13,22 @@ import { LICENSE_TYPE_TRIAL, } from '../../../common/constants'; import { CSV_JOB_TYPE as jobType } from '../../../constants'; -import { CreateJobFn, WorkerExecuteFn, ExportTypeDefinition } from '../../types'; -import { scheduleTaskFnFactory } from './create_job'; +import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; +import { createJobFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; import { metadata } from './metadata'; -import { JobParamsDiscoverCsv, ScheduledTaskParamsCSV } from './types'; +import { JobParamsDiscoverCsv, TaskPayloadCSV } from './types'; export const getExportType = (): ExportTypeDefinition< JobParamsDiscoverCsv, CreateJobFn, - ScheduledTaskParamsCSV, - WorkerExecuteFn + TaskPayloadCSV, + RunTaskFn > => ({ ...metadata, jobType, jobContentExtension: 'csv', - scheduleTaskFnFactory, + createJobFnFactory, runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, diff --git a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts index e0d09d04a3d3a..f420d8b033170 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CreateJobBaseParams, ScheduledTaskParams } from '../../types'; +import { BaseParams, BasePayload } from '../../types'; export type RawValue = string | object | null | undefined; @@ -28,7 +28,7 @@ export interface IndexPatternSavedObject { }; } -export interface JobParamsDiscoverCsv extends CreateJobBaseParams { +export interface JobParamsDiscoverCsv extends BaseParams { indexPatternId: string; title: string; searchRequest: SearchRequest; @@ -37,7 +37,7 @@ export interface JobParamsDiscoverCsv extends CreateJobBaseParams { conflictedTypesFields: string[]; } -export interface ScheduledTaskParamsCSV extends ScheduledTaskParams { +export interface TaskPayloadCSV extends BasePayload { basePath: string; searchRequest: any; fields: any; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts index 9acfc6d8c608d..1746792981a21 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts @@ -9,7 +9,7 @@ import { get } from 'lodash'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory } from '../../lib'; -import { ScheduleTaskFnFactory, TimeRangeParams } from '../../types'; +import { CreateJobFnFactory, TimeRangeParams } from '../../types'; import { JobParamsPanelCsv, SavedObject, @@ -37,7 +37,7 @@ interface VisData { panel: SearchPanel; } -export const scheduleTaskFnFactory: ScheduleTaskFnFactory = function createJobFactoryFn( +export const createJobFnFactory: CreateJobFnFactory = function createJobFactoryFn( reporting, parentLogger ) { @@ -45,7 +45,7 @@ export const scheduleTaskFnFactory: ScheduleTaskFnFactory const crypto = cryptoFactory(config.get('encryptionKey')); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']); - return async function scheduleTask(jobParams, headers, context, req) { + return async function createJob(jobParams, headers, context, req) { const { savedObjectType, savedObjectId } = jobParams; const serializedEncryptedHeaders = await crypto.encrypt(headers); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts index ec7e0a21f0498..3a5deda176b8c 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts @@ -7,16 +7,16 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { CancellationToken } from '../../../common'; import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; -import { RunTaskFnFactory, ScheduledTaskParams, TaskRunResult } from '../../types'; +import { BasePayload, RunTaskFnFactory, TaskRunResult } from '../../types'; import { createGenerateCsv } from '../csv/generate_csv'; -import { JobParamsPanelCsv, SearchPanel } from './types'; import { getGenerateCsvParams } from './lib/get_csv_job'; +import { JobParamsPanelCsv, SearchPanel } from './types'; /* * The run function receives the full request which provides the un-encrypted * headers, so encrypted headers are not part of these kind of job params */ -type ImmediateJobParams = Omit, 'headers'>; +type ImmediateJobParams = Omit, 'headers'>; /* * ImmediateExecuteFn receives the job doc payload because the payload was diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts index 7467f415299fa..4b4cfb3f062bf 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts @@ -14,16 +14,16 @@ import { } from '../../../common/constants'; import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; import { ExportTypeDefinition } from '../../types'; -import { metadata } from './metadata'; -import { ImmediateCreateJobFn, scheduleTaskFnFactory } from './create_job'; +import { createJobFnFactory, ImmediateCreateJobFn } from './create_job'; import { ImmediateExecuteFn, runTaskFnFactory } from './execute_job'; +import { metadata } from './metadata'; import { JobParamsPanelCsv } from './types'; /* * These functions are exported to share with the API route handler that * generates csv from saved object immediately on request. */ -export { scheduleTaskFnFactory } from './create_job'; +export { createJobFnFactory } from './create_job'; export { runTaskFnFactory } from './execute_job'; export const getExportType = (): ExportTypeDefinition< @@ -35,7 +35,7 @@ export const getExportType = (): ExportTypeDefinition< ...metadata, jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE, jobContentExtension: 'csv', - scheduleTaskFnFactory, + createJobFnFactory, runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts index 0d19a24114f06..9c45d23b13a37 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JobParamPostPayload, ScheduledTaskParams, TimeRangeParams } from '../../types'; +import { JobParamPostPayload, TimeRangeParams } from '../../types'; export interface FakeRequest { headers: Record; @@ -14,11 +14,20 @@ export interface JobParamsPostPayloadPanelCsv extends JobParamPostPayload { state?: any; } +export interface SearchPanel { + indexPatternSavedObjectId: string; + attributes: SavedSearchObjectAttributes; + timerange: TimeRangeParams; +} + +export interface JobPayloadPanelCsv extends JobParamsPanelCsv { + panel: SearchPanel; +} + export interface JobParamsPanelCsv { savedObjectType: string; savedObjectId: string; isImmediate: boolean; - panel?: SearchPanel; post?: JobParamsPostPayloadPanelCsv; visType?: string; } @@ -102,12 +111,6 @@ export interface VisPanel { timerange: TimeRangeParams; } -export interface SearchPanel { - indexPatternSavedObjectId: string; - attributes: SavedSearchObjectAttributes; - timerange: TimeRangeParams; -} - export interface DocValueFields { field: string; format: string; diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index 2252177e98085..173a67ad18edf 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -5,17 +5,17 @@ */ import { cryptoFactory } from '../../../lib'; -import { CreateJobFn, ScheduleTaskFnFactory } from '../../../types'; +import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; import { JobParamsPNG } from '../types'; -export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function scheduleTask( + return async function createJob( { objectType, title, relativeUrl, browserTimezone, layout }, context, req diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts index a0bd2d392187a..8a3f514ec7aea 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts @@ -9,8 +9,8 @@ import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common'; import { cryptoFactory, LevelLogger } from '../../../lib'; import { createMockReportingCore } from '../../../test_helpers'; -import { ScheduledTaskParamsPNG } from '../types'; import { generatePngObservableFactory } from '../lib/generate_png'; +import { TaskPayloadPNG } from '../types'; import { runTaskFnFactory } from './'; jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); @@ -36,7 +36,7 @@ const encryptHeaders = async (headers: Record) => { return await crypto.encrypt(headers); }; -const getScheduledTaskParams = (baseObj: any) => baseObj as ScheduledTaskParamsPNG; +const getBasePayload = (baseObj: any) => baseObj as TaskPayloadPNG; beforeEach(async () => { const kbnConfig = { @@ -85,7 +85,7 @@ test(`passes browserTimezone to generatePng`, async () => { const browserTimezone = 'UTC'; await runTask( 'pngJobId', - getScheduledTaskParams({ + getBasePayload({ relativeUrl: '/app/kibana#/something', browserTimezone, headers: encryptedHeaders, @@ -133,7 +133,7 @@ test(`returns content_type of application/png`, async () => { const { content_type: contentType } = await runTask( 'pngJobId', - getScheduledTaskParams({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), + getBasePayload({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), cancellationToken ); expect(contentType).toBe('image/png'); @@ -148,7 +148,7 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => { const encryptedHeaders = await encryptHeaders({}); const { content } = await runTask( 'pngJobId', - getScheduledTaskParams({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), + getBasePayload({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), cancellationToken ); diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts index 35cd4139df413..96b896b938962 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PNG_JOB_TYPE } from '../../../../common/constants'; -import { WorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../..//types'; +import { RunTaskFn, RunTaskFnFactory, TaskRunResult } from '../../..//types'; import { decryptJobHeaders, getConditionalHeaders, @@ -16,9 +16,9 @@ import { omitBlacklistedHeaders, } from '../../common'; import { generatePngObservableFactory } from '../lib/generate_png'; -import { ScheduledTaskParamsPNG } from '../types'; +import { TaskPayloadPNG } from '../types'; -type QueuedPngExecutorFactory = RunTaskFnFactory>; +type QueuedPngExecutorFactory = RunTaskFnFactory>; export const runTaskFnFactory: QueuedPngExecutorFactory = function executeJobFactoryFn( reporting, diff --git a/x-pack/plugins/reporting/server/export_types/png/index.ts b/x-pack/plugins/reporting/server/export_types/png/index.ts index c966dedb6b076..1cc6836572b7b 100644 --- a/x-pack/plugins/reporting/server/export_types/png/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/index.ts @@ -12,23 +12,23 @@ import { LICENSE_TYPE_TRIAL, PNG_JOB_TYPE as jobType, } from '../../../common/constants'; -import { CreateJobFn, WorkerExecuteFn, ExportTypeDefinition } from '../../types'; -import { scheduleTaskFnFactory } from './create_job'; +import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; +import { createJobFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; import { metadata } from './metadata'; -import { JobParamsPNG, ScheduledTaskParamsPNG } from './types'; +import { JobParamsPNG, TaskPayloadPNG } from './types'; export const getExportType = (): ExportTypeDefinition< JobParamsPNG, CreateJobFn, - ScheduledTaskParamsPNG, - WorkerExecuteFn + TaskPayloadPNG, + RunTaskFn > => ({ ...metadata, jobType, jobContentEncoding: 'base64', jobContentExtension: 'PNG', - scheduleTaskFnFactory, + createJobFnFactory, runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, diff --git a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts index 5969b5b8abc00..c3d5b2cc60051 100644 --- a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts @@ -10,7 +10,8 @@ import { map } from 'rxjs/operators'; import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; import { LayoutParams, PreserveLayout } from '../../../lib/layouts'; -import { ConditionalHeaders, ScreenshotResults } from '../../../types'; +import { ScreenshotResults } from '../../../lib/screenshots'; +import { ConditionalHeaders } from '../../../types'; export async function generatePngObservableFactory(reporting: ReportingCore) { const getScreenshots = await reporting.getScreenshotsObservable(); diff --git a/x-pack/plugins/reporting/server/export_types/png/types.d.ts b/x-pack/plugins/reporting/server/export_types/png/types.d.ts index 1ddee8419df30..a747f53861a99 100644 --- a/x-pack/plugins/reporting/server/export_types/png/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/png/types.d.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CreateJobBaseParams, ScheduledTaskParams } from '../../../server/types'; -import { LayoutInstance, LayoutParams } from '../../lib/layouts'; +import { BaseParams, BasePayload } from '../../../server/types'; +import { LayoutParams } from '../../lib/layouts'; // Job params: structure of incoming user request data -export interface JobParamsPNG extends CreateJobBaseParams { +export interface JobParamsPNG extends BaseParams { title: string; relativeUrl: string; } // Job payload: structure of stored job data provided by create_job -export interface ScheduledTaskParamsPNG extends ScheduledTaskParams { +export interface TaskPayloadPNG extends BasePayload { basePath?: string; browserTimezone: string; forceNow?: string; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index 5de089a13bfa4..96e634337e6a9 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -5,17 +5,17 @@ */ import { cryptoFactory } from '../../../lib'; -import { CreateJobFn, ScheduleTaskFnFactory } from '../../../types'; +import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; import { JobParamsPDF } from '../types'; -export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function scheduleTaskFn( + return async function createJob( { title, relativeUrls, browserTimezone, layout, objectType }, context, req diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts index 443db1b57fe48..fdc51dc1c9c87 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts @@ -12,7 +12,7 @@ import { CancellationToken } from '../../../../common'; import { cryptoFactory, LevelLogger } from '../../../lib'; import { createMockReportingCore } from '../../../test_helpers'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; -import { ScheduledTaskParamsPDF } from '../types'; +import { TaskPayloadPDF } from '../types'; import { runTaskFnFactory } from './'; let mockReporting: ReportingCore; @@ -36,7 +36,7 @@ const encryptHeaders = async (headers: Record) => { return await crypto.encrypt(headers); }; -const getScheduledTaskParams = (baseObj: any) => baseObj as ScheduledTaskParamsPDF; +const getBasePayload = (baseObj: any) => baseObj as TaskPayloadPDF; beforeEach(async () => { const kbnConfig = { @@ -83,7 +83,7 @@ test(`passes browserTimezone to generatePdf`, async () => { const browserTimezone = 'UTC'; await runTask( 'pdfJobId', - getScheduledTaskParams({ + getBasePayload({ title: 'PDF Params Timezone Test', relativeUrl: '/app/kibana#/something', browserTimezone, @@ -106,7 +106,7 @@ test(`returns content_type of application/pdf`, async () => { const { content_type: contentType } = await runTask( 'pdfJobId', - getScheduledTaskParams({ relativeUrls: [], headers: encryptedHeaders }), + getBasePayload({ relativeUrls: [], headers: encryptedHeaders }), cancellationToken ); expect(contentType).toBe('application/pdf'); @@ -121,7 +121,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const encryptedHeaders = await encryptHeaders({}); const { content } = await runTask( 'pdfJobId', - getScheduledTaskParams({ relativeUrls: [], headers: encryptedHeaders }), + getBasePayload({ relativeUrls: [], headers: encryptedHeaders }), cancellationToken ); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index 5ace1c987adb5..78808400c9c9a 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PDF_JOB_TYPE } from '../../../../common/constants'; -import { WorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../../types'; +import { RunTaskFn, RunTaskFnFactory, TaskRunResult } from '../../../types'; import { decryptJobHeaders, getConditionalHeaders, @@ -17,9 +17,9 @@ import { omitBlacklistedHeaders, } from '../../common'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; -import { ScheduledTaskParamsPDF } from '../types'; +import { TaskPayloadPDF } from '../types'; -type QueuedPdfExecutorFactory = RunTaskFnFactory>; +type QueuedPdfExecutorFactory = RunTaskFnFactory>; export const runTaskFnFactory: QueuedPdfExecutorFactory = function executeJobFactoryFn( reporting, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts index 7f21d36c4b72c..cf3ec9cdc8c2d 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts @@ -12,23 +12,23 @@ import { LICENSE_TYPE_TRIAL, PDF_JOB_TYPE as jobType, } from '../../../common/constants'; -import { CreateJobFn, WorkerExecuteFn, ExportTypeDefinition } from '../../types'; -import { scheduleTaskFnFactory } from './create_job'; +import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; +import { createJobFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; import { metadata } from './metadata'; -import { JobParamsPDF, ScheduledTaskParamsPDF } from './types'; +import { JobParamsPDF, TaskPayloadPDF } from './types'; export const getExportType = (): ExportTypeDefinition< JobParamsPDF, CreateJobFn, - ScheduledTaskParamsPDF, - WorkerExecuteFn + TaskPayloadPDF, + RunTaskFn > => ({ ...metadata, jobType, jobContentEncoding: 'base64', jobContentExtension: 'pdf', - scheduleTaskFnFactory, + createJobFnFactory, runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 2e0292e1f9ab5..17624c1bedb57 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -10,7 +10,8 @@ import { mergeMap } from 'rxjs/operators'; import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; import { createLayout, LayoutInstance, LayoutParams } from '../../../lib/layouts'; -import { ConditionalHeaders, ScreenshotResults } from '../../../types'; +import { ScreenshotResults } from '../../../lib/screenshots'; +import { ConditionalHeaders } from '../../../types'; // @ts-ignore untyped module import { pdf } from './pdf'; import { getTracker } from './tracker'; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts index 7830f87780c2e..3020cbb5f28b0 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts @@ -4,18 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CreateJobBaseParams, ScheduledTaskParams } from '../../../server/types'; +import { BaseParams, BasePayload } from '../../../server/types'; import { LayoutInstance, LayoutParams } from '../../lib/layouts'; // Job params: structure of incoming user request data, after being parsed from RISON -export interface JobParamsPDF extends CreateJobBaseParams { +export interface JobParamsPDF extends BaseParams { title: string; relativeUrls: string[]; layout: LayoutInstance; } // Job payload: structure of stored job data provided by create_job -export interface ScheduledTaskParamsPDF extends ScheduledTaskParams { +export interface TaskPayloadPDF extends BasePayload { basePath?: string; browserTimezone: string; forceNow?: string; diff --git a/x-pack/plugins/reporting/server/lib/create_worker.ts b/x-pack/plugins/reporting/server/lib/create_worker.ts index 5b0f1ddb2f157..dd5c560455274 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.ts @@ -8,7 +8,7 @@ import { CancellationToken } from '../../common'; import { PLUGIN_ID } from '../../common/constants'; import { ReportingCore } from '../../server'; import { LevelLogger } from '../../server/lib'; -import { ExportTypeDefinition, JobSource, WorkerExecuteFn } from '../../server/types'; +import { ExportTypeDefinition, JobSource, RunTaskFn } from '../../server/types'; import { ESQueueInstance } from './create_queue'; // @ts-ignore untyped dependency import { events as esqueueEvents } from './esqueue'; @@ -22,18 +22,18 @@ export function createWorkerFactory(reporting: ReportingCore, log // Once more document types are added, this will need to be passed in return async function createWorker(queue: ESQueueInstance) { // export type / execute job map - const jobExecutors: Map> = new Map(); + const jobExecutors: Map> = new Map(); for (const exportType of reporting.getExportTypesRegistry().getAll() as Array< - ExportTypeDefinition> + ExportTypeDefinition> >) { const jobExecutor = exportType.runTaskFnFactory(reporting, logger); jobExecutors.set(exportType.jobType, jobExecutor); } - const workerFn = ( - jobSource: JobSource, - jobParams: ScheduledTaskParamsType, + const workerFn = ( + jobSource: JobSource, + jobParams: TaskPayloadType, cancellationToken: CancellationToken ) => { const { diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index 3e3aef92c55a3..5acc6e38dddf9 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -6,13 +6,13 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { ReportingCore } from '../'; -import { CreateJobBaseParams, CreateJobFn, ReportingUser } from '../types'; +import { BaseParams, CreateJobFn, ReportingUser } from '../types'; import { LevelLogger } from './'; import { Report } from './store'; export type EnqueueJobFn = ( exportTypeId: string, - jobParams: CreateJobBaseParams, + jobParams: BaseParams, user: ReportingUser, context: RequestHandlerContext, request: KibanaRequest @@ -26,12 +26,12 @@ export function enqueueJobFactory( return async function enqueueJob( exportTypeId: string, - jobParams: CreateJobBaseParams, + jobParams: BaseParams, user: ReportingUser, context: RequestHandlerContext, request: KibanaRequest ) { - type ScheduleTaskFnType = CreateJobFn; + type CreateJobFnType = CreateJobFn; const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); @@ -39,13 +39,13 @@ export function enqueueJobFactory( throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); } - const [scheduleTask, { store }] = await Promise.all([ - exportType.scheduleTaskFnFactory(reporting, logger) as ScheduleTaskFnType, + const [createJob, { store }] = await Promise.all([ + exportType.createJobFnFactory(reporting, logger) as CreateJobFnType, reporting.getPluginStartDeps(), ]); // add encrytped headers - const payload = await scheduleTask(jobParams, context, request); + const payload = await createJob(jobParams, context, request); // store the pending report, puts it in the Reporting Management UI table const report = await store.addReport(exportType.jobType, user, payload); diff --git a/x-pack/plugins/reporting/server/lib/export_types_registry.ts b/x-pack/plugins/reporting/server/lib/export_types_registry.ts index 501989f21103e..1159221a9224e 100644 --- a/x-pack/plugins/reporting/server/lib/export_types_registry.ts +++ b/x-pack/plugins/reporting/server/lib/export_types_registry.ts @@ -11,8 +11,8 @@ import { getExportType as getTypePng } from '../export_types/png'; import { getExportType as getTypePrintablePdf } from '../export_types/printable_pdf'; import { ExportTypeDefinition } from '../types'; -type GetCallbackFn = ( - item: ExportTypeDefinition +type GetCallbackFn = ( + item: ExportTypeDefinition ) => boolean; // => ExportTypeDefinition @@ -21,8 +21,8 @@ export class ExportTypesRegistry { constructor() {} - register( - item: ExportTypeDefinition + register( + item: ExportTypeDefinition ): void { if (!isString(item.id)) { throw new Error(`'item' must have a String 'id' property `); @@ -43,24 +43,24 @@ export class ExportTypesRegistry { return this._map.size; } - getById( + getById( id: string - ): ExportTypeDefinition { + ): ExportTypeDefinition { if (!this._map.has(id)) { throw new Error(`Unknown id ${id}`); } return this._map.get(id) as ExportTypeDefinition< JobParamsType, - ScheduleTaskFnType, + CreateJobFnType, JobPayloadType, RunTaskFnType >; } - get( - findType: GetCallbackFn - ): ExportTypeDefinition { + get( + findType: GetCallbackFn + ): ExportTypeDefinition { let result; for (const value of this._map.values()) { if (!findType(value)) { @@ -68,7 +68,7 @@ export class ExportTypesRegistry { } const foundResult: ExportTypeDefinition< JobParamsType, - ScheduleTaskFnType, + CreateJobFnType, JobPayloadType, RunTaskFnType > = value; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts index 4fb9fd96ecfe6..6c619a726f428 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { AttributesMap, ElementsPositionAndAttribute } from '../../types'; import { LevelLogger, startTrace } from '../'; +import { HeadlessChromiumDriver } from '../../browsers'; import { LayoutInstance } from '../layouts'; +import { AttributesMap, ElementsPositionAndAttribute } from './'; import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; export const getElementPositionAndAttributes = async ( diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts index bc7b7005674a7..1ed8687bea23e 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { ElementsPositionAndAttribute, Screenshot } from '../../types'; import { LevelLogger, startTrace } from '../'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { ElementsPositionAndAttribute, Screenshot } from './'; export const getScreenshots = async ( browser: HeadlessChromiumDriver, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/lib/screenshots/index.ts index 5a04f1a497abf..c1d33cb519384 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/index.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/index.ts @@ -4,4 +4,61 @@ * you may not use this file except in compliance with the Elastic License. */ +import * as Rx from 'rxjs'; +import { LevelLogger } from '../'; +import { ConditionalHeaders } from '../../types'; +import { LayoutInstance } from '../layouts'; + export { screenshotsObservableFactory } from './observable'; + +export interface ScreenshotObservableOpts { + logger: LevelLogger; + urls: string[]; + conditionalHeaders: ConditionalHeaders; + layout: LayoutInstance; + browserTimezone: string; +} + +export interface AttributesMap { + [key: string]: any; +} + +export interface ElementPosition { + boundingClientRect: { + // modern browsers support x/y, but older ones don't + top: number; + left: number; + width: number; + height: number; + }; + scroll: { + x: number; + y: number; + }; +} + +export interface ElementsPositionAndAttribute { + position: ElementPosition; + attributes: AttributesMap; +} + +export interface Screenshot { + base64EncodedData: string; + title: string; + description: string; +} + +export interface ScreenshotResults { + timeRange: string | null; + screenshots: Screenshot[]; + error?: Error; + elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing +} + +export type ScreenshotsObservableFn = ({ + logger, + urls, + conditionalHeaders, + layout, + browserTimezone, +}: ScreenshotObservableOpts) => Rx.Observable; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index b25e8fab3abcf..3749e4372bdab 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -16,11 +16,12 @@ jest.mock('../../browsers/chromium/puppeteer', () => ({ })); import * as Rx from 'rxjs'; +import { LevelLogger } from '../'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { HeadlessChromiumDriver } from '../../browsers'; -import { LevelLogger } from '../'; import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../test_helpers'; -import { CaptureConfig, ConditionalHeaders, ElementsPositionAndAttribute } from '../../types'; +import { CaptureConfig, ConditionalHeaders } from '../../types'; +import { ElementsPositionAndAttribute } from './'; import * as contexts from './constants'; import { screenshotsObservableFactory } from './observable'; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index c6d3d826c88fb..342293d113d24 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -17,13 +17,13 @@ import { toArray, } from 'rxjs/operators'; import { HeadlessChromiumDriverFactory } from '../../browsers'; +import { CaptureConfig } from '../../types'; import { - CaptureConfig, ElementsPositionAndAttribute, ScreenshotObservableOpts, ScreenshotResults, ScreenshotsObservableFn, -} from '../../types'; +} from './'; import { checkPageIsOpen } from './check_browser_open'; import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; import { getElementPositionAndAttributes } from './get_element_position_data'; diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 88f6fa418a1b3..b1309cbdeb94d 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -7,11 +7,7 @@ import { ElasticsearchServiceSetup } from 'src/core/server'; import { LevelLogger, statuses } from '../'; import { ReportingCore } from '../../'; -import { - CreateJobBaseParams, - CreateJobBaseParamsEncryptedFields, - ReportingUser, -} from '../../types'; +import { BaseParams, BaseParamsEncryptedFields, ReportingUser } from '../../types'; import { indexTimestamp } from './index_timestamp'; import { mapping } from './mapping'; import { Report } from './report'; @@ -145,7 +141,7 @@ export class ReportingStore { public async addReport( type: string, user: ReportingUser, - payload: CreateJobBaseParams & CreateJobBaseParamsEncryptedFields + payload: BaseParams & BaseParamsEncryptedFields ): Promise { const timestamp = indexTimestamp(this.indexInterval); const index = `${this.indexPrefix}-${timestamp}`; diff --git a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts index f4959b56dfea1..36f0a7fe67d93 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import rison from 'rison-node'; import { schema } from '@kbn/config-schema'; -import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; -import { HandlerErrorFunction, HandlerFunction } from './types'; +import rison from 'rison-node'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; -import { CreateJobBaseParams } from '../types'; +import { BaseParams } from '../types'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { HandlerErrorFunction, HandlerFunction } from './types'; const BASE_GENERATE = `${API_BASE_URL}/generate`; @@ -70,7 +70,7 @@ export function registerGenerateFromJobParams( let jobParams; try { - jobParams = rison.decode(jobParamsRison) as CreateJobBaseParams | null; + jobParams = rison.decode(jobParamsRison) as BaseParams | null; if (!jobParams) { return res.customError({ statusCode: 400, diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index a0a8f25de7fc4..517f1dadc0ac1 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { KibanaRequest } from 'src/core/server'; import { ReportingCore } from '../'; import { API_BASE_GENERATE_V1 } from '../../common/constants'; -import { scheduleTaskFnFactory } from '../export_types/csv_from_savedobject/create_job'; +import { createJobFnFactory } from '../export_types/csv_from_savedobject/create_job'; import { runTaskFnFactory } from '../export_types/csv_from_savedobject/execute_job'; import { JobParamsPostPayloadPanelCsv } from '../export_types/csv_from_savedobject/types'; import { LevelLogger as Logger } from '../lib'; @@ -43,7 +43,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( /* * CSV export with the `immediate` option does not queue a job with Reporting's ESQueue to run the job async. Instead, this does: - * - re-use the scheduleTask function to build up es query config + * - re-use the createJob function to build up es query config * - re-use the runTask function to run the scan and scroll queries and capture the entire CSV in a result object. */ router.post( @@ -67,12 +67,12 @@ export function registerGenerateCsvFromSavedObjectImmediate( userHandler(async (user, context, req: CsvFromSavedObjectRequest, res) => { const logger = parentLogger.clone(['savedobject-csv']); const jobParams = getJobParamsFromRequest(req, { isImmediate: true }); - const scheduleTaskFn = scheduleTaskFnFactory(reporting, logger); + const createJob = createJobFnFactory(reporting, logger); const runTaskFn = runTaskFnFactory(reporting, logger); try { - // FIXME: no scheduleTaskFn for immediate download - const jobDocPayload = await scheduleTaskFn(jobParams, req.headers, context, req); + // FIXME: no create job for immediate download + const jobDocPayload = await createJob(jobParams, req.headers, context, req); const { content_type: jobOutputContentType, content: jobOutputContent, diff --git a/x-pack/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generation.test.ts index cef4da9aabbd4..0db0073149e57 100644 --- a/x-pack/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/plugins/reporting/server/routes/generation.test.ts @@ -75,7 +75,7 @@ describe('POST /api/reporting/generate', () => { jobContentEncoding: 'base64', jobContentExtension: 'pdf', validLicenses: ['basic', 'gold'], - scheduleTaskFnFactory: () => () => ({ scheduleParamsTest: { test1: 'yes' } }), + createJobFnFactory: () => () => ({ jobParamsTest: { test1: 'yes' } }), runTaskFnFactory: () => () => ({ runParamsTest: { test2: 'yes' } }), }); core.getExportTypesRegistry = () => mockExportTypesRegistry; diff --git a/x-pack/plugins/reporting/server/routes/types.d.ts b/x-pack/plugins/reporting/server/routes/types.d.ts index ab1dd841bbbc2..5c34d466197fe 100644 --- a/x-pack/plugins/reporting/server/routes/types.d.ts +++ b/x-pack/plugins/reporting/server/routes/types.d.ts @@ -5,12 +5,12 @@ */ import { KibanaRequest, KibanaResponseFactory, RequestHandlerContext } from 'src/core/server'; -import { CreateJobBaseParams, ReportingUser, ScheduledTaskParams } from '../types'; +import { BaseParams, BasePayload, ReportingUser } from '../types'; export type HandlerFunction = ( user: ReportingUser, exportType: string, - jobParams: CreateJobBaseParams, + jobParams: BaseParams, context: RequestHandlerContext, req: KibanaRequest, res: KibanaResponseFactory @@ -22,7 +22,7 @@ export interface QueuedJobPayload { error?: boolean; source: { job: { - payload: ScheduledTaskParams; + payload: BasePayload; }; }; } diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index 08313e6892f3c..f2785bce10964 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -8,8 +8,9 @@ import { Page } from 'puppeteer'; import * as Rx from 'rxjs'; import { chromium, HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../browsers'; import { LevelLogger } from '../lib'; +import { ElementsPositionAndAttribute } from '../lib/screenshots'; import * as contexts from '../lib/screenshots/constants'; -import { CaptureConfig, ElementsPositionAndAttribute } from '../types'; +import { CaptureConfig } from '../types'; interface CreateMockBrowserDriverFactoryOpts { evaluate: jest.Mock, any[]>; diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 542c9cdb3f677..10519842d9dec 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as Rx from 'rxjs'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DataPluginStart } from 'src/plugins/data/server/plugin'; @@ -48,7 +47,7 @@ export interface JobParamPostPayload { } // the pre-processed, encrypted data ready for storage -export interface ScheduledTaskParams { +export interface BasePayload { headers: string; // serialized encrypted headers jobParams: JobParamsType; title: string; @@ -61,7 +60,7 @@ export interface JobSource { _source: { jobtype: string; output: TaskRunResult; - payload: ScheduledTaskParams; + payload: BasePayload; status: JobStatus; }; } @@ -87,62 +86,6 @@ export interface ConditionalHeaders { conditions: ConditionalHeadersConditions; } -/* - * Screenshots - */ - -export interface ScreenshotObservableOpts { - logger: LevelLogger; - urls: string[]; - conditionalHeaders: ConditionalHeaders; - layout: LayoutInstance; - browserTimezone: string; -} - -export interface AttributesMap { - [key: string]: any; -} - -export interface ElementPosition { - boundingClientRect: { - // modern browsers support x/y, but older ones don't - top: number; - left: number; - width: number; - height: number; - }; - scroll: { - x: number; - y: number; - }; -} - -export interface ElementsPositionAndAttribute { - position: ElementPosition; - attributes: AttributesMap; -} - -export interface Screenshot { - base64EncodedData: string; - title: string; - description: string; -} - -export interface ScreenshotResults { - timeRange: string | null; - screenshots: Screenshot[]; - error?: Error; - elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing -} - -export type ScreenshotsObservableFn = ({ - logger, - urls, - conditionalHeaders, - layout, - browserTimezone, -}: ScreenshotObservableOpts) => Rx.Observable; - /* * Plugin Contract */ @@ -169,34 +112,33 @@ export type ReportingUser = { username: AuthenticatedUser['username'] } | false; export type CaptureConfig = ReportingConfigType['capture']; export type ScrollConfig = ReportingConfigType['csv']['scroll']; -export interface CreateJobBaseParams { +export interface BaseParams { browserTimezone: string; layout?: LayoutInstance; // for screenshot type reports objectType: string; } -export interface CreateJobBaseParamsEncryptedFields extends CreateJobBaseParams { +export interface BaseParamsEncryptedFields extends BaseParams { basePath?: string; // for screenshot type reports headers: string; // encrypted headers } -export type CreateJobFn = ( +export type CreateJobFn = ( jobParams: JobParamsType, context: RequestHandlerContext, request: KibanaRequest -) => Promise; +) => Promise; -// rename me -export type WorkerExecuteFn = ( +export type RunTaskFn = ( jobId: string, - job: ScheduledTaskParamsType, + job: TaskPayloadType, cancellationToken: CancellationToken ) => Promise; -export type ScheduleTaskFnFactory = ( +export type CreateJobFnFactory = ( reporting: ReportingCore, logger: LevelLogger -) => ScheduleTaskFnType; +) => CreateJobFnType; export type RunTaskFnFactory = ( reporting: ReportingCore, @@ -205,7 +147,7 @@ export type RunTaskFnFactory = ( export interface ExportTypeDefinition< JobParamsType, - ScheduleTaskFnType, + CreateJobFnType, JobPayloadType, RunTaskFnType > { @@ -214,7 +156,7 @@ export interface ExportTypeDefinition< jobType: string; jobContentEncoding?: string; jobContentExtension: string; - scheduleTaskFnFactory: ScheduleTaskFnFactory; + createJobFnFactory: CreateJobFnFactory; runTaskFnFactory: RunTaskFnFactory; validLicenses: string[]; } diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts index 40cbd00858b5f..0a795ad767898 100644 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts +++ b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.test.ts @@ -7,7 +7,7 @@ // @ts-ignore import fetchMock from 'fetch-mock/es5/client'; import { SessionTimeoutHttpInterceptor } from './session_timeout_http_interceptor'; -import { setup } from '../../../../../src/test_utils/public/http_test_setup'; +import { setup } from '../../../../../src/core/test_helpers/http_test_setup'; import { createSessionTimeoutMock } from './session_timeout.mock'; const mockCurrentUrl = (url: string) => window.history.pushState({}, '', url); diff --git a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts index 78c82cbc3a9a6..71ef6496ef6ca 100644 --- a/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts +++ b/x-pack/plugins/security/public/session/unauthorized_response_http_interceptor.test.ts @@ -7,7 +7,7 @@ // @ts-ignore import fetchMock from 'fetch-mock/es5/client'; import { SessionExpired } from './session_expired'; -import { setup } from '../../../../../src/test_utils/public/http_test_setup'; +import { setup } from '../../../../../src/core/test_helpers/http_test_setup'; import { UnauthorizedResponseHttpInterceptor } from './unauthorized_response_http_interceptor'; jest.mock('./session_expired'); diff --git a/x-pack/plugins/security_solution/common/detection_engine/parse_schedule_dates.ts b/x-pack/plugins/security_solution/common/detection_engine/parse_schedule_dates.ts new file mode 100644 index 0000000000000..b21cd84684fbc --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/parse_schedule_dates.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. + */ + +import moment from 'moment'; +import dateMath from '@elastic/datemath'; + +export const parseScheduleDates = (time: string): moment.Moment | null => { + const isValidDateString = !isNaN(Date.parse(time)); + const isValidInput = isValidDateString || time.trim().startsWith('now'); + const formattedDate = isValidDateString + ? moment(time) + : isValidInput + ? dateMath.parse(time) + : null; + + return formattedDate ?? null; +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 64f2f223a3073..e13aa3b8980e9 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -14,7 +14,7 @@ import { UUID } from '../types/uuid'; import { IsoDateString } from '../types/iso_date_string'; import { PositiveIntegerGreaterThanZero } from '../types/positive_integer_greater_than_zero'; import { PositiveInteger } from '../types/positive_integer'; -import { parseScheduleDates } from '../../utils'; +import { parseScheduleDates } from '../../parse_schedule_dates'; export const author = t.array(t.string); export type Author = t.TypeOf; @@ -338,7 +338,7 @@ export const sortFieldOrUndefined = t.union([sort_field, t.undefined]); export type SortFieldOrUndefined = t.TypeOf; export const sort_order = t.keyof({ asc: null, desc: null }); -export type sortOrder = t.TypeOf; +export type SortOrder = t.TypeOf; export const sortOrderOrUndefined = t.union([sort_order, t.undefined]); export type SortOrderOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts new file mode 100644 index 0000000000000..abfbc39189643 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts @@ -0,0 +1,18 @@ +/* + * 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 * from './add_prepackaged_rules_schema'; +export * from './create_rules_bulk_schema'; +export * from './create_rules_schema'; +export * from './export_rules_schema'; +export * from './find_rules_schema'; +export * from './import_rules_schema'; +export * from './patch_rules_bulk_schema'; +export * from './patch_rules_schema'; +export * from './query_rules_schema'; +export * from './query_signals_index_schema'; +export * from './set_signal_status_schema'; +export * from './update_rules_bulk_schema'; +export * from './update_rules_schema'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts new file mode 100644 index 0000000000000..6c22b8140e738 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.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. + */ +export * from './error_schema'; +export * from './find_rules_schema'; +export * from './import_rules_schema'; +export * from './prepackaged_rules_schema'; +export * from './prepackaged_rules_status_schema'; +export * from './rules_bulk_schema'; +export * from './rules_schema'; +export * from './type_timeline_only_schema'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/types.ts b/x-pack/plugins/security_solution/common/detection_engine/types.ts index 7c752bca49dbd..e7aab2fa5d219 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/types.ts @@ -3,18 +3,8 @@ * 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 { AlertAction } from '../../../alerts/common'; export type RuleAlertAction = Omit & { action_type_id: string; }; - -export const RuleTypeSchema = t.keyof({ - query: null, - saved_query: null, - machine_learning: null, - threshold: null, -}); -export type RuleType = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index a70258c2684b6..a72d641fbaf78 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; -import dateMath from '@elastic/datemath'; - import { EntriesArray } from '../shared_imports'; -import { RuleType } from './types'; +import { Type } from './schemas/common/schemas'; export const hasLargeValueList = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'list'); @@ -20,16 +17,4 @@ export const hasNestedEntry = (entries: EntriesArray): boolean => { return found.length > 0; }; -export const isThresholdRule = (ruleType: RuleType) => ruleType === 'threshold'; - -export const parseScheduleDates = (time: string): moment.Moment | null => { - const isValidDateString = !isNaN(Date.parse(time)); - const isValidInput = isValidDateString || time.trim().startsWith('now'); - const formattedDate = isValidDateString - ? moment(time) - : isValidInput - ? dateMath.parse(time) - : null; - - return formattedDate ?? null; -}; +export const isThresholdRule = (ruleType: Type) => ruleType === 'threshold'; diff --git a/x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.test.ts b/x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.test.ts new file mode 100644 index 0000000000000..9ba22e83b4b4d --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { extendMap } from './extend_map'; + +describe('ecs_fields test', () => { + describe('extendMap', () => { + test('it should extend a record', () => { + const osFieldsMap: Readonly> = { + 'os.platform': 'os.platform', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', + }; + const expected: Record = { + 'host.os.family': 'host.os.family', + 'host.os.full': 'host.os.full', + 'host.os.kernel': 'host.os.kernel', + 'host.os.platform': 'host.os.platform', + 'host.os.version': 'host.os.version', + }; + expect(extendMap('host', osFieldsMap)).toEqual(expected); + }); + + test('it should extend a sample hosts record', () => { + const hostMap: Record = { + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.name': 'host.name', + }; + const osFieldsMap: Readonly> = { + 'os.platform': 'os.platform', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', + }; + const expected: Record = { + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.name': 'host.name', + 'host.os.family': 'host.os.family', + 'host.os.full': 'host.os.full', + 'host.os.kernel': 'host.os.kernel', + 'host.os.platform': 'host.os.platform', + 'host.os.version': 'host.os.version', + }; + const output = { ...hostMap, ...extendMap('host', osFieldsMap) }; + expect(output).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.ts b/x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.ts new file mode 100644 index 0000000000000..c25979cbcdcee --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/ecs_fields/extend_map.ts @@ -0,0 +1,14 @@ +/* + * 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 const extendMap = ( + path: string, + map: Readonly> +): Readonly> => + Object.entries(map).reduce>((accum, [key, value]) => { + accum[`${path}.${key}`] = `${path}.${value}`; + return accum; + }, {}); diff --git a/x-pack/plugins/security_solution/common/ecs/ecs_fields/index.ts b/x-pack/plugins/security_solution/common/ecs/ecs_fields/index.ts new file mode 100644 index 0000000000000..19b16bd4bc6d2 --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/ecs_fields/index.ts @@ -0,0 +1,358 @@ +/* + * 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 { extendMap } from './extend_map'; + +export const auditdMap: Readonly> = { + 'auditd.result': 'auditd.result', + 'auditd.session': 'auditd.session', + 'auditd.data.acct': 'auditd.data.acct', + 'auditd.data.terminal': 'auditd.data.terminal', + 'auditd.data.op': 'auditd.data.op', + 'auditd.summary.actor.primary': 'auditd.summary.actor.primary', + 'auditd.summary.actor.secondary': 'auditd.summary.actor.secondary', + 'auditd.summary.object.primary': 'auditd.summary.object.primary', + 'auditd.summary.object.secondary': 'auditd.summary.object.secondary', + 'auditd.summary.object.type': 'auditd.summary.object.type', + 'auditd.summary.how': 'auditd.summary.how', + 'auditd.summary.message_type': 'auditd.summary.message_type', + 'auditd.summary.sequence': 'auditd.summary.sequence', +}; + +export const cloudFieldsMap: Readonly> = { + 'cloud.account.id': 'cloud.account.id', + 'cloud.availability_zone': 'cloud.availability_zone', + 'cloud.instance.id': 'cloud.instance.id', + 'cloud.instance.name': 'cloud.instance.name', + 'cloud.machine.type': 'cloud.machine.type', + 'cloud.provider': 'cloud.provider', + 'cloud.region': 'cloud.region', +}; + +export const fileMap: Readonly> = { + 'file.name': 'file.name', + 'file.path': 'file.path', + 'file.target_path': 'file.target_path', + 'file.extension': 'file.extension', + 'file.type': 'file.type', + 'file.device': 'file.device', + 'file.inode': 'file.inode', + 'file.uid': 'file.uid', + 'file.owner': 'file.owner', + 'file.gid': 'file.gid', + 'file.group': 'file.group', + 'file.mode': 'file.mode', + 'file.size': 'file.size', + 'file.mtime': 'file.mtime', + 'file.ctime': 'file.ctime', +}; + +export const osFieldsMap: Readonly> = { + 'os.platform': 'os.platform', + 'os.name': 'os.name', + 'os.full': 'os.full', + 'os.family': 'os.family', + 'os.version': 'os.version', + 'os.kernel': 'os.kernel', +}; + +export const hostFieldsMap: Readonly> = { + 'host.architecture': 'host.architecture', + 'host.id': 'host.id', + 'host.ip': 'host.ip', + 'host.mac': 'host.mac', + 'host.name': 'host.name', + ...extendMap('host', osFieldsMap), +}; + +export const processFieldsMap: Readonly> = { + 'process.hash.md5': 'process.hash.md5', + 'process.hash.sha1': 'process.hash.sha1', + 'process.hash.sha256': 'process.hash.sha256', + 'process.pid': 'process.pid', + 'process.name': 'process.name', + 'process.ppid': 'process.ppid', + 'process.args': 'process.args', + 'process.entity_id': 'process.entity_id', + 'process.executable': 'process.executable', + 'process.title': 'process.title', + 'process.thread': 'process.thread', + 'process.working_directory': 'process.working_directory', +}; + +export const agentFieldsMap: Readonly> = { + 'agent.type': 'agent.type', +}; + +export const userFieldsMap: Readonly> = { + 'user.domain': 'user.domain', + 'user.id': 'user.id', + 'user.name': 'user.name', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.full_name': 'user.full_name', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.email': 'user.email', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.hash': 'user.hash', + // NOTE: This field is not tested and available from ECS. Please remove this tag once it is + 'user.group': 'user.group', +}; + +export const winlogFieldsMap: Readonly> = { + 'winlog.event_id': 'winlog.event_id', +}; + +export const suricataFieldsMap: Readonly> = { + 'suricata.eve.flow_id': 'suricata.eve.flow_id', + 'suricata.eve.proto': 'suricata.eve.proto', + 'suricata.eve.alert.signature': 'suricata.eve.alert.signature', + 'suricata.eve.alert.signature_id': 'suricata.eve.alert.signature_id', +}; + +export const tlsFieldsMap: Readonly> = { + 'tls.client_certificate.fingerprint.sha1': 'tls.client_certificate.fingerprint.sha1', + 'tls.fingerprints.ja3.hash': 'tls.fingerprints.ja3.hash', + 'tls.server_certificate.fingerprint.sha1': 'tls.server_certificate.fingerprint.sha1', +}; + +export const urlFieldsMap: Readonly> = { + 'url.original': 'url.original', + 'url.domain': 'url.domain', + 'user.username': 'user.username', + 'user.password': 'user.password', +}; + +export const httpFieldsMap: Readonly> = { + 'http.version': 'http.version', + 'http.request': 'http.request', + 'http.request.method': 'http.request.method', + 'http.request.body.bytes': 'http.request.body.bytes', + 'http.request.body.content': 'http.request.body.content', + 'http.request.referrer': 'http.request.referrer', + 'http.response.status_code': 'http.response.status_code', + 'http.response.body': 'http.response.body', + 'http.response.body.bytes': 'http.response.body.bytes', + 'http.response.body.content': 'http.response.body.content', +}; + +export const zeekFieldsMap: Readonly> = { + 'zeek.session_id': 'zeek.session_id', + 'zeek.connection.local_resp': 'zeek.connection.local_resp', + 'zeek.connection.local_orig': 'zeek.connection.local_orig', + 'zeek.connection.missed_bytes': 'zeek.connection.missed_bytes', + 'zeek.connection.state': 'zeek.connection.state', + 'zeek.connection.history': 'zeek.connection.history', + 'zeek.notice.suppress_for': 'zeek.notice.suppress_for', + 'zeek.notice.msg': 'zeek.notice.msg', + 'zeek.notice.note': 'zeek.notice.note', + 'zeek.notice.sub': 'zeek.notice.sub', + 'zeek.notice.dst': 'zeek.notice.dst', + 'zeek.notice.dropped': 'zeek.notice.dropped', + 'zeek.notice.peer_descr': 'zeek.notice.peer_descr', + 'zeek.dns.AA': 'zeek.dns.AA', + 'zeek.dns.qclass_name': 'zeek.dns.qclass_name', + 'zeek.dns.RD': 'zeek.dns.RD', + 'zeek.dns.qtype_name': 'zeek.dns.qtype_name', + 'zeek.dns.qtype': 'zeek.dns.qtype', + 'zeek.dns.query': 'zeek.dns.query', + 'zeek.dns.trans_id': 'zeek.dns.trans_id', + 'zeek.dns.qclass': 'zeek.dns.qclass', + 'zeek.dns.RA': 'zeek.dns.RA', + 'zeek.dns.TC': 'zeek.dns.TC', + 'zeek.http.resp_mime_types': 'zeek.http.resp_mime_types', + 'zeek.http.trans_depth': 'zeek.http.trans_depth', + 'zeek.http.status_msg': 'zeek.http.status_msg', + 'zeek.http.resp_fuids': 'zeek.http.resp_fuids', + 'zeek.http.tags': 'zeek.http.tags', + 'zeek.files.session_ids': 'zeek.files.session_ids', + 'zeek.files.timedout': 'zeek.files.timedout', + 'zeek.files.local_orig': 'zeek.files.local_orig', + 'zeek.files.tx_host': 'zeek.files.tx_host', + 'zeek.files.source': 'zeek.files.source', + 'zeek.files.is_orig': 'zeek.files.is_orig', + 'zeek.files.overflow_bytes': 'zeek.files.overflow_bytes', + 'zeek.files.sha1': 'zeek.files.sha1', + 'zeek.files.duration': 'zeek.files.duration', + 'zeek.files.depth': 'zeek.files.depth', + 'zeek.files.analyzers': 'zeek.files.analyzers', + 'zeek.files.mime_type': 'zeek.files.mime_type', + 'zeek.files.rx_host': 'zeek.files.rx_host', + 'zeek.files.total_bytes': 'zeek.files.total_bytes', + 'zeek.files.fuid': 'zeek.files.fuid', + 'zeek.files.seen_bytes': 'zeek.files.seen_bytes', + 'zeek.files.missing_bytes': 'zeek.files.missing_bytes', + 'zeek.files.md5': 'zeek.files.md5', + 'zeek.ssl.cipher': 'zeek.ssl.cipher', + 'zeek.ssl.established': 'zeek.ssl.established', + 'zeek.ssl.resumed': 'zeek.ssl.resumed', + 'zeek.ssl.version': 'zeek.ssl.version', +}; + +export const sourceFieldsMap: Readonly> = { + 'source.bytes': 'source.bytes', + 'source.ip': 'source.ip', + 'source.packets': 'source.packets', + 'source.port': 'source.port', + 'source.domain': 'source.domain', + 'source.geo.continent_name': 'source.geo.continent_name', + 'source.geo.country_name': 'source.geo.country_name', + 'source.geo.country_iso_code': 'source.geo.country_iso_code', + 'source.geo.city_name': 'source.geo.city_name', + 'source.geo.region_iso_code': 'source.geo.region_iso_code', + 'source.geo.region_name': 'source.geo.region_name', +}; + +export const destinationFieldsMap: Readonly> = { + 'destination.bytes': 'destination.bytes', + 'destination.ip': 'destination.ip', + 'destination.packets': 'destination.packets', + 'destination.port': 'destination.port', + 'destination.domain': 'destination.domain', + 'destination.geo.continent_name': 'destination.geo.continent_name', + 'destination.geo.country_name': 'destination.geo.country_name', + 'destination.geo.country_iso_code': 'destination.geo.country_iso_code', + 'destination.geo.city_name': 'destination.geo.city_name', + 'destination.geo.region_iso_code': 'destination.geo.region_iso_code', + 'destination.geo.region_name': 'destination.geo.region_name', +}; + +export const networkFieldsMap: Readonly> = { + 'network.bytes': 'network.bytes', + 'network.community_id': 'network.community_id', + 'network.direction': 'network.direction', + 'network.packets': 'network.packets', + 'network.protocol': 'network.protocol', + 'network.transport': 'network.transport', +}; + +export const geoFieldsMap: Readonly> = { + 'geo.region_name': 'destination.geo.region_name', + 'geo.country_iso_code': 'destination.geo.country_iso_code', +}; + +export const dnsFieldsMap: Readonly> = { + 'dns.question.name': 'dns.question.name', + 'dns.question.type': 'dns.question.type', + 'dns.resolved_ip': 'dns.resolved_ip', + 'dns.response_code': 'dns.response_code', +}; + +export const endgameFieldsMap: Readonly> = { + 'endgame.exit_code': 'endgame.exit_code', + 'endgame.file_name': 'endgame.file_name', + 'endgame.file_path': 'endgame.file_path', + 'endgame.logon_type': 'endgame.logon_type', + 'endgame.parent_process_name': 'endgame.parent_process_name', + 'endgame.pid': 'endgame.pid', + 'endgame.process_name': 'endgame.process_name', + 'endgame.subject_domain_name': 'endgame.subject_domain_name', + 'endgame.subject_logon_id': 'endgame.subject_logon_id', + 'endgame.subject_user_name': 'endgame.subject_user_name', + 'endgame.target_domain_name': 'endgame.target_domain_name', + 'endgame.target_logon_id': 'endgame.target_logon_id', + 'endgame.target_user_name': 'endgame.target_user_name', +}; + +export const eventBaseFieldsMap: Readonly> = { + 'event.action': 'event.action', + 'event.category': 'event.category', + 'event.code': 'event.code', + 'event.created': 'event.created', + 'event.dataset': 'event.dataset', + 'event.duration': 'event.duration', + 'event.end': 'event.end', + 'event.hash': 'event.hash', + 'event.id': 'event.id', + 'event.kind': 'event.kind', + 'event.module': 'event.module', + 'event.original': 'event.original', + 'event.outcome': 'event.outcome', + 'event.risk_score': 'event.risk_score', + 'event.risk_score_norm': 'event.risk_score_norm', + 'event.severity': 'event.severity', + 'event.start': 'event.start', + 'event.timezone': 'event.timezone', + 'event.type': 'event.type', +}; + +export const systemFieldsMap: Readonly> = { + 'system.audit.package.arch': 'system.audit.package.arch', + 'system.audit.package.entity_id': 'system.audit.package.entity_id', + 'system.audit.package.name': 'system.audit.package.name', + 'system.audit.package.size': 'system.audit.package.size', + 'system.audit.package.summary': 'system.audit.package.summary', + 'system.audit.package.version': 'system.audit.package.version', + 'system.auth.ssh.signature': 'system.auth.ssh.signature', + 'system.auth.ssh.method': 'system.auth.ssh.method', +}; + +export const signalFieldsMap: Readonly> = { + 'signal.original_time': 'signal.original_time', + 'signal.rule.id': 'signal.rule.id', + 'signal.rule.saved_id': 'signal.rule.saved_id', + 'signal.rule.timeline_id': 'signal.rule.timeline_id', + 'signal.rule.timeline_title': 'signal.rule.timeline_title', + 'signal.rule.output_index': 'signal.rule.output_index', + 'signal.rule.from': 'signal.rule.from', + 'signal.rule.index': 'signal.rule.index', + 'signal.rule.language': 'signal.rule.language', + 'signal.rule.query': 'signal.rule.query', + 'signal.rule.to': 'signal.rule.to', + 'signal.rule.filters': 'signal.rule.filters', + 'signal.rule.rule_id': 'signal.rule.rule_id', + 'signal.rule.false_positives': 'signal.rule.false_positives', + 'signal.rule.max_signals': 'signal.rule.max_signals', + 'signal.rule.risk_score': 'signal.rule.risk_score', + 'signal.rule.description': 'signal.rule.description', + 'signal.rule.name': 'signal.rule.name', + 'signal.rule.immutable': 'signal.rule.immutable', + 'signal.rule.references': 'signal.rule.references', + 'signal.rule.severity': 'signal.rule.severity', + 'signal.rule.tags': 'signal.rule.tags', + 'signal.rule.threat': 'signal.rule.threat', + 'signal.rule.type': 'signal.rule.type', + 'signal.rule.size': 'signal.rule.size', + 'signal.rule.enabled': 'signal.rule.enabled', + 'signal.rule.created_at': 'signal.rule.created_at', + 'signal.rule.updated_at': 'signal.rule.updated_at', + 'signal.rule.created_by': 'signal.rule.created_by', + 'signal.rule.updated_by': 'signal.rule.updated_by', + 'signal.rule.version': 'signal.rule.version', + 'signal.rule.note': 'signal.rule.note', + 'signal.rule.threshold': 'signal.rule.threshold', + 'signal.rule.exceptions_list': 'signal.rule.exceptions_list', +}; + +export const ruleFieldsMap: Readonly> = { + 'rule.reference': 'rule.reference', +}; + +export const eventFieldsMap: Readonly> = { + timestamp: '@timestamp', + '@timestamp': '@timestamp', + message: 'message', + ...{ ...agentFieldsMap }, + ...{ ...auditdMap }, + ...{ ...destinationFieldsMap }, + ...{ ...dnsFieldsMap }, + ...{ ...endgameFieldsMap }, + ...{ ...eventBaseFieldsMap }, + ...{ ...fileMap }, + ...{ ...geoFieldsMap }, + ...{ ...hostFieldsMap }, + ...{ ...networkFieldsMap }, + ...{ ...ruleFieldsMap }, + ...{ ...signalFieldsMap }, + ...{ ...sourceFieldsMap }, + ...{ ...suricataFieldsMap }, + ...{ ...systemFieldsMap }, + ...{ ...tlsFieldsMap }, + ...{ ...zeekFieldsMap }, + ...{ ...httpFieldsMap }, + ...{ ...userFieldsMap }, + ...{ ...winlogFieldsMap }, + ...{ ...processFieldsMap }, +}; diff --git a/x-pack/plugins/security_solution/common/ecs/index.ts b/x-pack/plugins/security_solution/common/ecs/index.ts index ff21ebc5ef973..e31d42b02f80b 100644 --- a/x-pack/plugins/security_solution/common/ecs/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/index.ts @@ -27,52 +27,28 @@ import { SystemEcs } from './system'; export interface Ecs { _id: string; - _index?: string; - auditd?: AuditdEcs; - destination?: DestinationEcs; - dns?: DnsEcs; - endgame?: EndgameEcs; - event?: EventEcs; - geo?: GeoEcs; - host?: HostEcs; - network?: NetworkEcs; - rule?: RuleEcs; - signal?: SignalEcs; - source?: SourceEcs; - suricata?: SuricataEcs; - tls?: TlsEcs; - zeek?: ZeekEcs; - http?: HttpEcs; - url?: UrlEcs; - timestamp?: string; - message?: string[]; - user?: UserEcs; - winlog?: WinlogEcs; - process?: ProcessEcs; - file?: File; - system?: SystemEcs; } diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index b72a52f0a0eb7..366bf7a1df1f2 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -14,3 +14,4 @@ export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100; export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps'; export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps'; +export const TRUSTED_APPS_DELETE_API = '/api/endpoint/trusted_apps/{id}'; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 7535b23a10e8a..7c0de84b637c9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -6,6 +6,12 @@ import { schema } from '@kbn/config-schema'; +export const DeleteTrustedAppsRequestSchema = { + params: schema.object({ + id: schema.string(), + }), +}; + export const GetTrustedAppsRequestSchema = { query: schema.object({ page: schema.maybe(schema.number({ defaultValue: 1, min: 1 })), diff --git a/x-pack/plugins/security_solution/common/machine_learning/helpers.ts b/x-pack/plugins/security_solution/common/machine_learning/helpers.ts index fe3eb79a6f610..73dc30f238c7e 100644 --- a/x-pack/plugins/security_solution/common/machine_learning/helpers.ts +++ b/x-pack/plugins/security_solution/common/machine_learning/helpers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RuleType } from '../detection_engine/types'; +import { Type } from '../detection_engine/schemas/common/schemas'; // Based on ML Job/Datafeed States from x-pack/legacy/plugins/ml/common/constants/states.js const enabledStates = ['started', 'opened']; @@ -23,4 +23,4 @@ export const isJobFailed = (jobState: string, datafeedState: string): boolean => return failureStates.includes(jobState) || failureStates.includes(datafeedState); }; -export const isMlRule = (ruleType: RuleType) => ruleType === 'machine_learning'; +export const isMlRule = (ruleType: Type) => ruleType === 'machine_learning'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts new file mode 100644 index 0000000000000..0576871a2bf81 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts @@ -0,0 +1,119 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../src/plugins/data/common'; + +export type Maybe = T | null; + +export type SearchHit = IEsSearchResponse['rawResponse']['hits']['hits'][0]; + +export interface TotalValue { + value: number; + relation: string; +} + +export interface Inspect { + dsl: string[]; +} + +export interface PageInfoPaginated { + activePage: number; + fakeTotalCount: number; + showMorePagesIndicator: boolean; +} + +export interface CursorType { + value?: Maybe; + tiebreaker?: Maybe; +} + +export enum Direction { + asc = 'asc', + desc = 'desc', +} + +export interface SortField { + field: Field; + direction: Direction; +} + +export interface TimerangeInput { + /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ + interval: string; + /** The end of the timerange */ + to: string; + /** The beginning of the timerange */ + from: string; +} + +export interface PaginationInput { + /** The limit parameter allows you to configure the maximum amount of items to be returned */ + limit: number; + /** The cursor parameter defines the next result you want to fetch */ + cursor?: Maybe; + /** The tiebreaker parameter allow to be more precise to fetch the next item */ + tiebreaker?: Maybe; +} + +export interface PaginationInputPaginated { + /** The activePage parameter defines the page of results you want to fetch */ + activePage: number; + /** The cursorStart parameter defines the start of the results to be displayed */ + cursorStart: number; + /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ + fakePossibleCount: number; + /** The querySize parameter is the number of items to be returned */ + querySize: number; +} + +export interface DocValueFields { + field: string; + format: string; +} + +export interface Explanation { + value: number; + description: string; + details: Explanation[]; +} + +export interface TotalValue { + value: number; + relation: string; +} +export interface ShardsResponse { + total: number; + successful: number; + failed: number; + skipped: number; +} + +export interface TotalHit { + value: number; + relation: string; +} + +export interface Hit { + _index: string; + _type: string; + _id: string; + _score: number | null; +} + +export interface Hits { + hits: { + total: T; + max_score: number | null; + hits: U[]; + }; +} + +export interface GenericBuckets { + key: string; + doc_count: number; +} + +export type StringOrNumber = string | number; diff --git a/x-pack/plugins/security_solution/common/search_strategy/index.ts b/x-pack/plugins/security_solution/common/search_strategy/index.ts new file mode 100644 index 0000000000000..cff9f4ca2f029 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './common'; +export * from './security_solution'; +export * from './timeline'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/all/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/all/index.ts index 91a53066b4f4b..5ddcd8da30efb 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/all/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/all/index.ts @@ -7,7 +7,8 @@ import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; import { HostItem, HostsFields } from '../common'; -import { CursorType, Inspect, Maybe, PageInfoPaginated, RequestOptionsPaginated } from '../..'; +import { CursorType, Inspect, Maybe, PageInfoPaginated } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; export interface HostsEdges { node: HostItem; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts new file mode 100644 index 0000000000000..efdc96b33562a --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts @@ -0,0 +1,83 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { UserEcs } from '../../../../ecs/user'; +import { SourceEcs } from '../../../../ecs/source'; +import { HostEcs } from '../../../../ecs/host'; +import { + CursorType, + Inspect, + Maybe, + PageInfoPaginated, + StringOrNumber, + Hit, + TotalHit, +} from '../../../common'; +import { RequestOptionsPaginated } from '../../'; + +export interface HostAuthenticationsStrategyResponse extends IEsSearchResponse { + edges: AuthenticationsEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface HostAuthenticationsRequestOptions extends RequestOptionsPaginated { + defaultIndex: string[]; +} + +export interface AuthenticationsEdges { + node: AuthenticationItem; + cursor: CursorType; +} + +export interface AuthenticationItem { + _id: string; + failures: number; + successes: number; + user: UserEcs; + lastSuccess?: Maybe; + lastFailure?: Maybe; +} + +export interface LastSourceHost { + timestamp?: Maybe; + source?: Maybe; + host?: Maybe; +} + +export interface AuthenticationHit extends Hit { + _source: { + '@timestamp': string; + lastSuccess?: LastSourceHost; + lastFailure?: LastSourceHost; + }; + user: string; + failures: number; + successes: number; + cursor?: string; + sort: StringOrNumber[]; +} + +export interface AuthenticationBucket { + key: { + user_uid: string; + }; + doc_count: number; + failures: { + doc_count: number; + }; + successes: { + doc_count: number; + }; + authentication: { + hits: { + total: TotalHit; + hits: ArrayLike; + }; + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts index d15da4bf07ae7..902e9909cf728 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -6,7 +6,7 @@ import { CloudEcs } from '../../../../ecs/cloud'; import { HostEcs, OsEcs } from '../../../../ecs/host'; -import { Maybe, SearchHit, TotalValue } from '../..'; +import { Hit, Hits, Maybe, SearchHit, StringOrNumber, TotalValue } from '../../../common'; export enum HostPolicyResponseActionStatus { success = 'success', @@ -98,3 +98,15 @@ export interface HostAggEsData extends SearchHit { sort: string[]; aggregations: HostAggEsItem; } + +export interface HostHit extends Hit { + _source: { + '@timestamp'?: string; + host: HostEcs; + }; + cursor?: string; + firstSeen?: string; + sort?: StringOrNumber[]; +} + +export type HostHits = Hits; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts index cbabe9dd11115..adf70a109bc03 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts @@ -5,7 +5,8 @@ */ import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; -import { Inspect, Maybe, RequestOptionsPaginated } from '../..'; +import { Inspect, Maybe } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; import { HostsFields } from '../common'; export interface HostFirstLastSeenRequestOptions diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index a27899e454074..f5d46078fcea4 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -4,13 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './authentications'; export * from './all'; export * from './common'; export * from './overview'; export * from './first_last_seen'; +export * from './uncommon_processes'; export enum HostsQueries { + authentications = 'authentications', + firstLastSeen = 'firstLastSeen', hosts = 'hosts', hostOverview = 'hostOverview', - firstLastSeen = 'firstLastSeen', + uncommonProcesses = 'uncommonProcesses', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts index 8d54481f56dbd..7d212a951905a 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts @@ -5,9 +5,9 @@ */ import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; - +import { Inspect, Maybe, TimerangeInput } from '../../../common'; import { HostItem, HostsFields } from '../common'; -import { Inspect, Maybe, RequestOptionsPaginated, TimerangeInput } from '../..'; +import { RequestOptionsPaginated } from '../..'; export interface HostOverviewStrategyResponse extends IEsSearchResponse { hostOverview: HostItem; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts new file mode 100644 index 0000000000000..28c0ccb7f6f4f --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts @@ -0,0 +1,86 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { HostEcs } from '../../../../ecs/host'; +import { UserEcs } from '../../../../ecs/user'; +import { + RequestOptionsPaginated, + SortField, + CursorType, + Inspect, + Maybe, + PageInfoPaginated, + Hit, + TotalHit, + StringOrNumber, + Hits, +} from '../../..'; + +export interface HostUncommonProcessesRequestOptions extends RequestOptionsPaginated { + sort: SortField; + defaultIndex: string[]; +} + +export interface HostUncommonProcessesStrategyResponse extends IEsSearchResponse { + edges: UncommonProcessesEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface UncommonProcessesEdges { + node: UncommonProcessItem; + cursor: CursorType; +} + +export interface UncommonProcessItem { + _id: string; + instances: number; + process: ProcessEcsFields; + hosts: HostEcs[]; + user?: Maybe; +} + +export interface ProcessEcsFields { + hash?: Maybe; + pid?: Maybe; + name?: Maybe; + ppid?: Maybe; + args?: Maybe; + entity_id?: Maybe; + executable?: Maybe; + title?: Maybe; + thread?: Maybe; + working_directory?: Maybe; +} + +export interface ProcessHashData { + md5?: Maybe; + sha1?: Maybe; + sha256?: Maybe; +} + +export interface Thread { + id?: Maybe; + start?: Maybe; +} + +export interface UncommonProcessHit extends Hit { + total: TotalHit; + host: Array<{ + id: string[] | undefined; + name: string[] | undefined; + }>; + _source: { + '@timestamp': string; + process: ProcessEcsFields; + }; + cursor: string; + sort: StringOrNumber[]; +} + +export type ProcessHits = Hits; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 175784bc5ade5..17adf38559a9e 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -4,92 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IEsSearchRequest, IEsSearchResponse } from '../../../../../../src/plugins/data/common'; +import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; import { ESQuery } from '../../typed_json'; import { HostOverviewStrategyResponse, + HostAuthenticationsRequestOptions, + HostAuthenticationsStrategyResponse, HostOverviewRequestOptions, HostFirstLastSeenStrategyResponse, HostFirstLastSeenRequestOptions, HostsQueries, HostsRequestOptions, HostsStrategyResponse, + HostUncommonProcessesStrategyResponse, + HostUncommonProcessesRequestOptions, } from './hosts'; -import { NetworkQueries, NetworkTlsStrategyResponse, NetworkTlsRequestOptions } from './network'; +import { + NetworkQueries, + NetworkTlsStrategyResponse, + NetworkTlsRequestOptions, + NetworkHttpStrategyResponse, + NetworkHttpRequestOptions, + NetworkTopCountriesStrategyResponse, + NetworkTopCountriesRequestOptions, +} from './network'; +import { + DocValueFields, + TimerangeInput, + SortField, + PaginationInput, + PaginationInputPaginated, +} from '../common'; export * from './hosts'; export * from './network'; -export type Maybe = T | null; export type FactoryQueryTypes = HostsQueries | NetworkQueries; -export type SearchHit = IEsSearchResponse['rawResponse']['hits']['hits'][0]; - -export interface TotalValue { - value: number; - relation: string; -} - -export interface Inspect { - dsl: string[]; - response: string[]; -} - -export interface PageInfoPaginated { - activePage: number; - fakeTotalCount: number; - showMorePagesIndicator: boolean; -} - -export interface CursorType { - value?: Maybe; - tiebreaker?: Maybe; -} - -export enum Direction { - asc = 'asc', - desc = 'desc', -} - -export interface SortField { - field: Field; - direction: Direction; -} - -export interface TimerangeInput { - /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ - interval: string; - /** The end of the timerange */ - to: string; - /** The beginning of the timerange */ - from: string; -} - -export interface PaginationInput { - /** The limit parameter allows you to configure the maximum amount of items to be returned */ - limit: number; - /** The cursor parameter defines the next result you want to fetch */ - cursor?: Maybe; - /** The tiebreaker parameter allow to be more precise to fetch the next item */ - tiebreaker?: Maybe; -} - -export interface PaginationInputPaginated { - /** The activePage parameter defines the page of results you want to fetch */ - activePage: number; - /** The cursorStart parameter defines the start of the results to be displayed */ - cursorStart: number; - /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ - fakePossibleCount: number; - /** The querySize parameter is the number of items to be returned */ - querySize: number; -} - -export interface DocValueFields { - field: string; - format: string; -} - export interface RequestBasicOptions extends IEsSearchRequest { timerange: TimerangeInput; filterQuery: ESQuery | string | undefined; @@ -98,6 +49,8 @@ export interface RequestBasicOptions extends IEsSearchRequest { factoryQueryType?: FactoryQueryTypes; } +/** A mapping of semantic fields to their document counterparts */ + export interface RequestOptions extends RequestBasicOptions { pagination: PaginationInput; sort: SortField; @@ -112,18 +65,34 @@ export type StrategyResponseType = T extends HostsQ ? HostsStrategyResponse : T extends HostsQueries.hostOverview ? HostOverviewStrategyResponse + : T extends HostsQueries.authentications + ? HostAuthenticationsStrategyResponse : T extends HostsQueries.firstLastSeen ? HostFirstLastSeenStrategyResponse + : T extends HostsQueries.uncommonProcesses + ? HostUncommonProcessesStrategyResponse : T extends NetworkQueries.tls ? NetworkTlsStrategyResponse + : T extends NetworkQueries.http + ? NetworkHttpStrategyResponse + : T extends NetworkQueries.topCountries + ? NetworkTopCountriesStrategyResponse : never; export type StrategyRequestType = T extends HostsQueries.hosts ? HostsRequestOptions : T extends HostsQueries.hostOverview ? HostOverviewRequestOptions + : T extends HostsQueries.authentications + ? HostAuthenticationsRequestOptions : T extends HostsQueries.firstLastSeen ? HostFirstLastSeenRequestOptions + : T extends HostsQueries.uncommonProcesses + ? HostUncommonProcessesRequestOptions : T extends NetworkQueries.tls ? NetworkTlsRequestOptions + : T extends NetworkQueries.http + ? NetworkHttpRequestOptions + : T extends NetworkQueries.topCountries + ? NetworkTopCountriesRequestOptions : never; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts new file mode 100644 index 0000000000000..a6ae956a42187 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts @@ -0,0 +1,10 @@ +/* + * 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 enum FlowTargetSourceDest { + destination = 'destination', + source = 'source', +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts new file mode 100644 index 0000000000000..ad58442b16994 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { Maybe, CursorType, Inspect, PageInfoPaginated, GenericBuckets } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; + +export interface NetworkHttpRequestOptions extends RequestOptionsPaginated { + ip?: string; + defaultIndex: string[]; +} + +export interface NetworkHttpStrategyResponse extends IEsSearchResponse { + edges: NetworkHttpEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface NetworkHttpEdges { + node: NetworkHttpItem; + cursor: CursorType; +} + +export interface NetworkHttpItem { + _id?: Maybe; + domains: string[]; + lastHost?: Maybe; + lastSourceIp?: Maybe; + methods: string[]; + path?: Maybe; + requestCount?: Maybe; + statuses: string[]; +} + +export interface NetworkHttpBuckets { + key: string; + doc_count: number; + domains: { + buckets: GenericBuckets[]; + }; + methods: { + buckets: GenericBuckets[]; + }; + source: object; + status: { + buckets: GenericBuckets[]; + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts index 680a3697ef0bd..ac5e6fdacc94b 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts @@ -4,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './common'; +export * from './http'; export * from './tls'; +export * from './top_countries'; export enum NetworkQueries { + http = 'http', tls = 'tls', + topCountries = 'topCountries', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts index c9e593bb7a7d2..dffc994fcf4cb 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts @@ -5,7 +5,9 @@ */ import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; -import { CursorType, Inspect, Maybe, PageInfoPaginated, RequestOptionsPaginated } from '../..'; +import { CursorType, Inspect, Maybe, PageInfoPaginated } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; +import { FlowTargetSourceDest } from '../common'; export interface TlsBuckets { key: string; @@ -36,11 +38,6 @@ export interface TlsNode { issuers?: Maybe; } -export enum FlowTargetSourceDest { - destination = 'destination', - source = 'source', -} - export enum TlsFields { _id = '_id', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts new file mode 100644 index 0000000000000..3188a26dd69fd --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts @@ -0,0 +1,99 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { GeoEcs } from '../../../../ecs/geo'; +import { CursorType, Inspect, Maybe, PageInfoPaginated } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; +import { FlowTargetSourceDest } from '../common'; + +export enum NetworkTopTablesFields { + bytes_in = 'bytes_in', + bytes_out = 'bytes_out', + flows = 'flows', + destination_ips = 'destination_ips', + source_ips = 'source_ips', +} + +export enum NetworkDnsFields { + dnsName = 'dnsName', + queryCount = 'queryCount', + uniqueDomains = 'uniqueDomains', + dnsBytesIn = 'dnsBytesIn', + dnsBytesOut = 'dnsBytesOut', +} + +export enum FlowTarget { + client = 'client', + destination = 'destination', + server = 'server', + source = 'source', +} + +export interface GeoItem { + geo?: Maybe; + flowTarget?: Maybe; +} + +export interface TopCountriesItemSource { + country?: Maybe; + destination_ips?: Maybe; + flows?: Maybe; + location?: Maybe; + source_ips?: Maybe; +} + +export interface NetworkTopCountriesRequestOptions + extends RequestOptionsPaginated { + flowTarget: FlowTargetSourceDest; + ip?: string; +} + +export interface NetworkTopCountriesStrategyResponse extends IEsSearchResponse { + edges: NetworkTopCountriesEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface NetworkTopCountriesEdges { + node: NetworkTopCountriesItem; + cursor: CursorType; +} + +export interface NetworkTopCountriesItem { + _id?: Maybe; + source?: Maybe; + destination?: Maybe; + network?: Maybe; +} + +export interface TopCountriesItemDestination { + country?: Maybe; + destination_ips?: Maybe; + flows?: Maybe; + location?: Maybe; + source_ips?: Maybe; +} + +export interface TopNetworkTablesEcsField { + bytes_in?: Maybe; + bytes_out?: Maybe; +} + +export interface NetworkTopCountriesBuckets { + country: string; + key: string; + bytes_in: { + value: number; + }; + bytes_out: { + value: number; + }; + flows: number; + destination_ips: number; + source_ips: number; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/details/index.ts new file mode 100644 index 0000000000000..e5e1c41f4731a --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/details/index.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 { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; +import { Inspect, Maybe } from '../../common'; +import { TimelineRequestOptionsPaginated } from '..'; + +export interface DetailItem { + field: string; + values?: Maybe; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + originalValue?: Maybe; +} + +export interface TimelineDetailsStrategyResponse extends IEsSearchResponse { + data?: Maybe; + inspect?: Maybe; +} + +export interface TimelineDetailsRequestOptions extends Partial { + defaultIndex: string[]; + executeQuery: boolean; + indexName: string; + eventId: string; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts new file mode 100644 index 0000000000000..a7bf61c102cd4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts @@ -0,0 +1,70 @@ +/* + * 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 { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; +import { ESQuery } from '../../typed_json'; +import { Ecs } from '../../ecs'; +import { + CursorType, + Maybe, + TimerangeInput, + DocValueFields, + PaginationInput, + PaginationInputPaginated, + SortField, +} from '../common'; +import { TimelineDetailsRequestOptions, TimelineDetailsStrategyResponse } from './details'; + +export * from './details'; + +export enum TimelineQueries { + details = 'details', +} + +export type TimelineFactoryQueryTypes = TimelineQueries; + +export interface TimelineEdges { + node: TimelineItem; + cursor: CursorType; +} + +export interface TimelineItem { + _id: string; + _index?: Maybe; + data: TimelineNonEcsData[]; + ecs: Ecs; +} + +export interface TimelineNonEcsData { + field: string; + value?: Maybe; +} + +export interface TimelineRequestBasicOptions extends IEsSearchRequest { + timerange: TimerangeInput; + filterQuery: ESQuery | string | undefined; + defaultIndex: string[]; + docValueFields?: DocValueFields[]; + factoryQueryType?: TimelineFactoryQueryTypes; +} + +export interface TimelineRequestOptions extends TimelineRequestBasicOptions { + pagination: PaginationInput; + sortField?: SortField; +} + +export interface TimelineRequestOptionsPaginated extends TimelineRequestBasicOptions { + pagination: PaginationInputPaginated; + sortField?: SortField; +} + +export type TimelineStrategyResponseType< + T extends TimelineFactoryQueryTypes +> = T extends TimelineQueries.details ? TimelineDetailsStrategyResponse : never; + +export type TimelineStrategyRequestType< + T extends TimelineFactoryQueryTypes +> = T extends TimelineQueries.details ? TimelineDetailsRequestOptions : never; diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index e28d1969b3976..564254b6a7596 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -25,7 +25,6 @@ export { NamespaceType, Operator, OperatorEnum, - OperatorType, OperatorTypeEnum, ExceptionListTypeEnum, exceptionListItemSchema, diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js index 4f382f13bcd5d..f0dd797601176 100644 --- a/x-pack/plugins/security_solution/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -45,15 +45,14 @@ Cypress.Commands.add( prevSubject: 'element', }, (input, fileName, fileType = 'text/plain') => { - cy.fixture(fileName) - .then((content) => Cypress.Blob.base64StringToBlob(content, fileType)) - .then((blob) => { - const testFile = new File([blob], fileName, { type: fileType }); - const dataTransfer = new DataTransfer(); + cy.fixture(fileName).then((content) => { + const blob = Cypress.Blob.base64StringToBlob(content, fileType); + const testFile = new File([blob], fileName, { type: fileType }); + const dataTransfer = new DataTransfer(); - dataTransfer.items.add(testFile); - input[0].files = dataTransfer.files; - return input; - }); + dataTransfer.items.add(testFile); + input[0].files = dataTransfer.files; + return input; + }); } ); diff --git a/x-pack/plugins/security_solution/cypress/support/index.js b/x-pack/plugins/security_solution/cypress/support/index.js index 42abecd4b0bad..244781e0ccd01 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.js +++ b/x-pack/plugins/security_solution/cypress/support/index.js @@ -23,7 +23,7 @@ import './commands'; Cypress.Cookies.defaults({ - whitelist: 'sid', + preserve: 'sid', }); Cypress.on('uncaught:exception', (err) => { diff --git a/x-pack/plugins/security_solution/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json index 929a3fb39babb..d0669ccb78281 100644 --- a/x-pack/plugins/security_solution/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/cypress/tsconfig.json @@ -1,10 +1,11 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../../../tsconfig.base.json", "exclude": [], "include": [ "./**/*" ], "compilerOptions": { + "tsBuildInfoFile": "../../../../build/tsbuildinfo/security_solution/cypress", "types": [ "cypress", "node" diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 1cc50b7d951a2..8068d51a80153 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -9,7 +9,7 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; -import { DetailItem } from '../../../graphql/types'; +import { DetailItem } from '../../../../common/search_strategy/timeline'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; import { EventFieldsBrowser } from './event_fields_browser'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index 00a4e581320bb..9737a09c89f49 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -10,7 +10,7 @@ import React, { useMemo } from 'react'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { BrowserFields, getAllFieldsByName } from '../../containers/source'; -import { DetailItem } from '../../../graphql/types'; +import { DetailItem } from '../../../../common/search_strategy/timeline'; import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; import { getColumns } from './columns'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx index 0bb0532eee7be..f4028c988acb8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/stateful_event_details.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useState } from 'react'; import { BrowserFields } from '../../containers/source'; -import { DetailItem } from '../../../graphql/types'; +import { DetailItem } from '../../../../common/search_strategy/timeline'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx index 8e2b47bd91dbc..100c5e46141a2 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; +import { + FlowTarget, + FlowTargetSourceDest, +} from '../../../../common/search_strategy/security_solution/network'; import { appendSearch } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 2f7aa1b14cfda..943f2d8336ca7 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -32,7 +32,10 @@ import { getCreateCaseUrl, useFormatUrl, } from '../link_to'; -import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; +import { + FlowTarget, + FlowTargetSourceDest, +} from '../../../../common/search_strategy/security_solution/network'; import { useUiSetting$, useKibana } from '../../lib/kibana'; import { isUrlInvalid } from '../../utils/validators'; import { ExternalLinkIcon } from '../external_link_icon'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap index 60088a77878ad..a40ccfa7cd1b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap @@ -100,7 +100,11 @@ exports[`JobsTableComponent renders correctly against snapshot 1`] = ` ] } loading={true} - noItemsMessage={} + noItemsMessage={ + + } onChange={[Function]} pagination={ Object { diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx index be911a1cd8537..1e9e689dcd6ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx @@ -125,7 +125,7 @@ export const JobsTableComponent = ({ isLoading, jobs, onJobStateChange }: JobTab columns={getJobsTableColumns(isLoading, onJobStateChange, basePath)} items={getPaginatedItems(jobs, pageIndex, pageSize)} loading={isLoading} - noItemsMessage={} + noItemsMessage={} pagination={pagination} responsive={false} onChange={({ page }: { page: { index: number } }) => { @@ -141,13 +141,13 @@ export const JobsTable = React.memo(JobsTableComponent); JobsTable.displayName = 'JobsTable'; -export const NoItemsMessage = React.memo(() => ( +export const NoItemsMessage = React.memo(({ basePath }: { basePath: string }) => ( {i18n.NO_ITEMS_TEXT}} titleSize="xs" actions={ = ({ data: nonEcsRowData, fieldName: 'signal.rule.type', }); - const [ruleType] = ruleTypes as RuleType[]; + const [ruleType] = ruleTypes as Type[]; return !isMlRule(ruleType) && !isThresholdRule(ruleType); }, [nonEcsRowData]); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index 0d98a0f2f26ff..073cb46d3949a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -140,7 +140,11 @@ describe('helpers', () => { filterManager: mockFilterManager, query: mockQueryBarWithFilters.query, savedId: mockQueryBarWithFilters.saved_id, - indexPatterns: { fields: [{ name: 'test name', type: 'test type' }], title: 'test title' }, + indexPatterns: { + fields: [{ name: 'event.category', type: 'test type' }], + title: 'test title', + getFormatterForField: () => ({ convert: (val: unknown) => val }), + }, }); const wrapper = shallow(result[0].description as React.ReactElement); const filterLabelComponent = wrapper.find(esFilters.FilterLabel).at(0); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 3a0a5b04c5874..680ca78fc222e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -24,8 +24,7 @@ import styled from 'styled-components'; import { assertUnreachable } from '../../../../../common/utility_types'; import * as i18nSeverity from '../severity_mapping/translations'; import * as i18nRiskScore from '../risk_score_mapping/translations'; -import { Threshold } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { RuleType } from '../../../../../common/detection_engine/types'; +import { Threshold, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; @@ -357,7 +356,7 @@ export const buildNoteDescription = (label: string, note: string): ListItems[] = return []; }; -export const buildRuleTypeDescription = (label: string, ruleType: RuleType): ListItems[] => { +export const buildRuleTypeDescription = (label: string, ruleType: Type): ListItems[] => { switch (ruleType) { case 'machine_learning': { return [ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 00141c9a453d8..cf27fa97b1479 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -9,7 +9,6 @@ import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; import React, { memo, useState } from 'react'; import styled from 'styled-components'; -import { RuleType } from '../../../../../common/detection_engine/types'; import { IIndexPattern, Filter, @@ -42,6 +41,7 @@ import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/ import { buildMlJobDescription } from './ml_job_description'; import { buildActionsDescription } from './actions_description'; import { buildThrottleDescription } from './throttle_description'; +import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; const DescriptionListContainer = styled(EuiDescriptionList)` &.euiDescriptionList--column .euiDescriptionList__title { @@ -211,7 +211,7 @@ export const getDescriptionItem = ( const val: string = get(field, data); return buildNoteDescription(label, val); } else if (field === 'ruleType') { - const ruleType: RuleType = get(field, data); + const ruleType: Type = get(field, data); return buildRuleTypeDescription(label, ruleType); } else if (field === 'kibanaSiemAppUrl') { return []; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx index c6ea269e1a355..6f5f3361d2b3e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/select_rule_type/index.tsx @@ -9,11 +9,11 @@ import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiFormRow, EuiIcon } from '@elastic import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../common/detection_engine/utils'; -import { RuleType } from '../../../../../common/detection_engine/types'; import { FieldHook } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; import { MlCardDescription } from './ml_card_description'; +import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; interface SelectRuleTypeProps { describedByIds?: string[]; @@ -30,9 +30,9 @@ export const SelectRuleType: React.FC = ({ hasValidLicense = false, isMlAdmin = false, }) => { - const ruleType = field.value as RuleType; + const ruleType = field.value as Type; const setType = useCallback( - (type: RuleType) => { + (type: Type) => { field.setValue(type); }, [field] diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts index f12a5d523bade..0ed091f2c18a6 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts @@ -5,9 +5,9 @@ */ import { - AddRulesProps, PatchRuleProps, - NewRule, + CreateRulesProps, + UpdateRulesProps, PrePackagedRulesStatusResponse, BasicFetchProps, RuleStatusResponse, @@ -16,13 +16,18 @@ import { FetchRulesResponse, FetchRulesProps, } from '../types'; -import { ruleMock, savedRuleMock, rulesMock } from '../mock'; +import { savedRuleMock, rulesMock } from '../mock'; +import { getRulesSchemaMock } from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; +import { RulesSchema } from '../../../../../../common/detection_engine/schemas/response'; -export const addRule = async ({ rule, signal }: AddRulesProps): Promise => - Promise.resolve(ruleMock); +export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise => + Promise.resolve(getRulesSchemaMock()); -export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise => - Promise.resolve(ruleMock); +export const createRule = async ({ rule, signal }: CreateRulesProps): Promise => + Promise.resolve(getRulesSchemaMock()); + +export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise => + Promise.resolve(getRulesSchemaMock()); export const getPrePackagedRulesStatus = async ({ signal, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index f58c95ed71e29..cd1ded544cfe5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -6,7 +6,9 @@ import { KibanaServices } from '../../../../common/lib/kibana'; import { - addRule, + createRule, + updateRule, + patchRule, fetchRules, fetchRuleById, enableRules, @@ -19,9 +21,12 @@ import { fetchTags, getPrePackagedRulesStatus, } from './api'; -import { ruleMock, rulesMock } from './mock'; +import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; +import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock'; +import { rulesMock } from './mock'; import { buildEsQuery } from 'src/plugins/data/common'; - const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; jest.mock('../../../../common/lib/kibana'); @@ -30,25 +35,56 @@ const fetchMock = jest.fn(); mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); describe('Detections Rules API', () => { - describe('addRule', () => { + describe('createRule', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(ruleMock); + fetchMock.mockResolvedValue(getRulesSchemaMock()); }); - test('check parameter url, body', async () => { - await addRule({ rule: ruleMock, signal: abortCtrl.signal }); + test('POSTs rule', async () => { + const payload = getCreateRulesSchemaMock(); + await createRule({ rule: payload, signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { body: - '{"description":"some desc","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf","language":"kuery","risk_score":75,"name":"Test rule","query":"user.email: \'root@elastic.co\'","references":[],"severity":"high","tags":["APM"],"to":"now","type":"query","threat":[],"throttle":null}', + '{"description":"Detecting root and admin users","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1"}', method: 'POST', signal: abortCtrl.signal, }); }); + }); - test('happy path', async () => { - const ruleResp = await addRule({ rule: ruleMock, signal: abortCtrl.signal }); - expect(ruleResp).toEqual(ruleMock); + describe('updateRule', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(getRulesSchemaMock()); + }); + + test('PUTs rule', async () => { + const payload = getUpdateRulesSchemaMock(); + await updateRule({ rule: payload, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { + body: + '{"description":"some description","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1"}', + method: 'PUT', + signal: abortCtrl.signal, + }); + }); + }); + + describe('patchRule', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(getRulesSchemaMock()); + }); + + test('PATCHs rule', async () => { + const payload = getPatchRulesSchemaMock(); + await patchRule({ ruleProperties: payload, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { + body: JSON.stringify(payload), + method: 'PATCH', + signal: abortCtrl.signal, + }); }); }); @@ -280,7 +316,7 @@ describe('Detections Rules API', () => { describe('fetchRuleById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(ruleMock); + fetchMock.mockResolvedValue(getRulesSchemaMock()); }); test('check parameter url, query', async () => { @@ -296,7 +332,7 @@ describe('Detections Rules API', () => { test('happy path', async () => { const ruleResp = await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); - expect(ruleResp).toEqual(ruleMock); + expect(ruleResp).toEqual(getRulesSchemaMock()); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 3538d8ec8c9b9..e254516d11076 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { HttpStart } from '../../../../../../../../src/core/public'; import { DETECTION_ENGINE_RULES_URL, @@ -13,13 +12,13 @@ import { DETECTION_ENGINE_TAGS_URL, } from '../../../../../common/constants'; import { - AddRulesProps, + UpdateRulesProps, + CreateRulesProps, DeleteRulesProps, DuplicateRulesProps, EnableRulesProps, FetchRulesProps, FetchRulesResponse, - NewRule, Rule, FetchRuleProps, BasicFetchProps, @@ -33,32 +32,51 @@ import { } from './types'; import { KibanaServices } from '../../../../common/lib/kibana'; import * as i18n from '../../../pages/detection_engine/rules/translations'; +import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; /** - * Add provided Rule + * Create provided Rule * - * @param rule to add + * @param rule CreateRulesSchema to add * @param signal to cancel request * * @throws An error if response is not OK */ -export const addRule = async ({ rule, signal }: AddRulesProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { - method: rule.id != null ? 'PUT' : 'POST', +export const createRule = async ({ rule, signal }: CreateRulesProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { + method: 'POST', + body: JSON.stringify(rule), + signal, + }); + +/** + * Update provided Rule using PUT + * + * @param rule UpdateRulesSchema to be updated + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { + method: 'PUT', body: JSON.stringify(rule), signal, }); /** - * Patch provided Rule + * Patch provided rule + * NOTE: The rule edit flow does NOT use patch as it relies on the + * functionality of PUT to delete field values when not provided, if + * just expecting changes, use this `patchRule` * * @param ruleProperties to patch * @param signal to cancel request * * @throws An error if response is not OK */ -export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { +export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { method: 'PATCH', body: JSON.stringify(ruleProperties), signal, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts index c7ecfb33cd905..a40ab2e487851 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts @@ -6,7 +6,8 @@ export * from './api'; export * from './fetch_index_patterns'; -export * from './persist_rule'; +export * from './use_update_rule'; +export * from './use_create_rule'; export * from './types'; export * from './use_rule'; export * from './use_rules'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts index fa11cfabcdf8b..c0397b0af6db9 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts @@ -4,36 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NewRule, FetchRulesResponse, Rule } from './types'; - -export const ruleMock: NewRule = { - description: 'some desc', - enabled: true, - false_positives: [], - filters: [], - from: 'now-360s', - index: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - interval: '5m', - rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', - language: 'kuery', - risk_score: 75, - name: 'Test rule', - query: "user.email: 'root@elastic.co'", - references: [], - severity: 'high', - tags: ['APM'], - to: 'now', - type: 'query', - threat: [], - throttle: null, -}; +import { FetchRulesResponse, Rule } from './types'; export const savedRuleMock: Rule = { author: [], diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index a5bbc0465ccc1..e94e57ad82bcf 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -6,8 +6,8 @@ import * as t from 'io-ts'; -import { RuleTypeSchema } from '../../../../../common/detection_engine/types'; import { + SortOrder, author, building_block_type, license, @@ -16,12 +16,14 @@ import { severity_mapping, timestamp_override, threshold, + type, } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { listArray } from '../../../../../common/detection_engine/schemas/types'; import { - listArray, - listArrayOrUndefined, -} from '../../../../../common/detection_engine/schemas/types'; -import { PatchRulesSchema } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema'; + CreateRulesSchema, + PatchRulesSchema, + UpdateRulesSchema, +} from '../../../../../common/detection_engine/schemas/request'; /** * Params is an "record", since it is a type of AlertActionParams which is action templates. @@ -36,48 +38,13 @@ export const action = t.exact( }) ); -export const NewRuleSchema = t.intersection([ - t.type({ - description: t.string, - enabled: t.boolean, - interval: t.string, - name: t.string, - risk_score: t.number, - severity: t.string, - type: RuleTypeSchema, - }), - t.partial({ - actions: t.array(action), - anomaly_threshold: t.number, - created_by: t.string, - false_positives: t.array(t.string), - filters: t.array(t.unknown), - from: t.string, - id: t.string, - index: t.array(t.string), - language: t.string, - machine_learning_job_id: t.string, - max_signals: t.number, - query: t.string, - references: t.array(t.string), - rule_id: t.string, - saved_id: t.string, - tags: t.array(t.string), - threat: t.array(t.unknown), - threshold, - throttle: t.union([t.string, t.null]), - to: t.string, - updated_by: t.string, - note: t.string, - exceptions_list: listArrayOrUndefined, - }), -]); - -export const NewRulesSchema = t.array(NewRuleSchema); -export type NewRule = t.TypeOf; +export interface CreateRulesProps { + rule: CreateRulesSchema; + signal: AbortSignal; +} -export interface AddRulesProps { - rule: NewRule; +export interface UpdateRulesProps { + rule: UpdateRulesSchema; signal: AbortSignal; } @@ -117,7 +84,7 @@ export const RuleSchema = t.intersection([ severity: t.string, severity_mapping, tags: t.array(t.string), - type: RuleTypeSchema, + type, to: t.string, threat: t.array(t.unknown), updated_at: t.string, @@ -185,7 +152,7 @@ export interface FetchRulesProps { export interface FilterOptions { filter: string; sortField: string; - sortOrder: 'asc' | 'desc'; + sortOrder: SortOrder; showCustomRules?: boolean; showElasticRules?: boolean; tags?: string[]; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx similarity index 68% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx index 1bf21623992e6..42d6a2a92a4c2 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx @@ -6,25 +6,25 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { usePersistRule, ReturnPersistRule } from './persist_rule'; -import { ruleMock } from './mock'; +import { useCreateRule, ReturnCreateRule } from './use_create_rule'; +import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; jest.mock('./api'); -describe('usePersistRule', () => { +describe('useCreateRule', () => { test('init', async () => { - const { result } = renderHook(() => usePersistRule()); + const { result } = renderHook(() => useCreateRule()); expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); }); test('saving rule with isLoading === true', async () => { await act(async () => { - const { result, rerender, waitForNextUpdate } = renderHook(() => - usePersistRule() + const { result, rerender, waitForNextUpdate } = renderHook(() => + useCreateRule() ); await waitForNextUpdate(); - result.current[1](ruleMock); + result.current[1](getUpdateRulesSchemaMock()); rerender(); expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); @@ -32,11 +32,11 @@ describe('usePersistRule', () => { test('saved rule with isSaved === true', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePersistRule() + const { result, waitForNextUpdate } = renderHook(() => + useCreateRule() ); await waitForNextUpdate(); - result.current[1](ruleMock); + result.current[1](getUpdateRulesSchemaMock()); await waitForNextUpdate(); expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx similarity index 76% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx index fd139d59c0a27..2bbd27994fc77 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx @@ -7,20 +7,20 @@ import { useEffect, useState, Dispatch } from 'react'; import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; +import { CreateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; -import { addRule as persistRule } from './api'; +import { createRule } from './api'; import * as i18n from './translations'; -import { NewRule } from './types'; -interface PersistRuleReturn { +interface CreateRuleReturn { isLoading: boolean; isSaved: boolean; } -export type ReturnPersistRule = [PersistRuleReturn, Dispatch]; +export type ReturnCreateRule = [CreateRuleReturn, Dispatch]; -export const usePersistRule = (): ReturnPersistRule => { - const [rule, setRule] = useState(null); +export const useCreateRule = (): ReturnCreateRule => { + const [rule, setRule] = useState(null); const [isSaved, setIsSaved] = useState(false); const [isLoading, setIsLoading] = useState(false); const [, dispatchToaster] = useStateToaster(); @@ -33,7 +33,7 @@ export const usePersistRule = (): ReturnPersistRule => { if (rule != null) { try { setIsLoading(true); - await persistRule({ rule, signal: abortCtrl.signal }); + await createRule({ rule, signal: abortCtrl.signal }); if (isSubscribed) { setIsSaved(true); } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx index 6721d89f2799b..2ba78cd90cf9b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx @@ -9,7 +9,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import * as api from './api'; -import { ruleMock } from './mock'; +import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { ReturnUseDissasociateExceptionList, UseDissasociateExceptionListProps, @@ -23,7 +23,7 @@ describe('useDissasociateExceptionList', () => { const onSuccess = jest.fn(); beforeEach(() => { - jest.spyOn(api, 'patchRule').mockResolvedValue(ruleMock); + jest.spyOn(api, 'patchRule').mockResolvedValue(getRulesSchemaMock()); }); afterEach(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx index 9a6ea4f60fdcc..92d46a785b034 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -10,7 +10,7 @@ import * as api from './api'; jest.mock('./api'); -describe('usePersistRule', () => { +describe('usePrePackagedRules', () => { beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx new file mode 100644 index 0000000000000..9603a4151933a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx @@ -0,0 +1,44 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { useUpdateRule, ReturnUpdateRule } from './use_update_rule'; +import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; + +jest.mock('./api'); + +describe('useUpdateRule', () => { + test('init', async () => { + const { result } = renderHook(() => useUpdateRule()); + + expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); + }); + + test('saving rule with isLoading === true', async () => { + await act(async () => { + const { result, rerender, waitForNextUpdate } = renderHook(() => + useUpdateRule() + ); + await waitForNextUpdate(); + result.current[1](getUpdateRulesSchemaMock()); + rerender(); + expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); + }); + }); + + test('saved rule with isSaved === true', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateRule() + ); + await waitForNextUpdate(); + result.current[1](getUpdateRulesSchemaMock()); + await waitForNextUpdate(); + expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx new file mode 100644 index 0000000000000..a437974e93ba3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx @@ -0,0 +1,60 @@ +/* + * 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 { useEffect, useState, Dispatch } from 'react'; + +import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; +import { UpdateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; + +import { updateRule } from './api'; +import * as i18n from './translations'; + +interface UpdateRuleReturn { + isLoading: boolean; + isSaved: boolean; +} + +export type ReturnUpdateRule = [UpdateRuleReturn, Dispatch]; + +export const useUpdateRule = (): ReturnUpdateRule => { + const [rule, setRule] = useState(null); + const [isSaved, setIsSaved] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setIsSaved(false); + async function saveRule() { + if (rule != null) { + try { + setIsLoading(true); + await updateRule({ rule, signal: abortCtrl.signal }); + if (isSubscribed) { + setIsSaved(true); + } + } catch (error) { + if (isSubscribed) { + errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster }); + } + } + if (isSubscribed) { + setIsLoading(false); + } + } + } + + saveRule(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rule]); + + return [{ isLoading, isSaved }, setRule]; +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index d6dc97fbae158..79488231b29ee 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -5,7 +5,8 @@ */ import { List } from '../../../../../../common/detection_engine/schemas/types'; -import { NewRule } from '../../../../containers/detection_engine/rules'; +import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request/create_rules_schema'; +import { Rule } from '../../../../containers/detection_engine/rules'; import { getListMock, getEndpointListMock, @@ -721,13 +722,13 @@ describe('helpers', () => { mockActions = mockActionsStepRule(); }); - test('returns NewRule with type of saved_query when saved_id exists', () => { - const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions); + test('returns rule with type of saved_query when saved_id exists', () => { + const result: Rule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions); expect(result.type).toEqual('saved_query'); }); - test('returns NewRule with type of query when saved_id does not exist', () => { + test('returns rule with type of query when saved_id does not exist', () => { const mockDefineStepRuleWithoutSavedId = { ...mockDefine, queryBar: { @@ -735,7 +736,7 @@ describe('helpers', () => { saved_id: '', }, }; - const result: NewRule = formatRule( + const result: CreateRulesSchema = formatRule( mockDefineStepRuleWithoutSavedId, mockAbout, mockSchedule, @@ -745,10 +746,15 @@ describe('helpers', () => { expect(result.type).toEqual('query'); }); - test('returns NewRule without id if ruleId does not exist', () => { - const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions); + test('returns rule without id if ruleId does not exist', () => { + const result: CreateRulesSchema = formatRule( + mockDefine, + mockAbout, + mockSchedule, + mockActions + ); - expect(result.id).toBeUndefined(); + expect(result).not.toHaveProperty('id'); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 434a33ac37722..874ef032b7c5e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -10,12 +10,12 @@ import deepmerge from 'deepmerge'; import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/constants'; import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; -import { RuleType } from '../../../../../../common/detection_engine/types'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; import { List } from '../../../../../../common/detection_engine/schemas/types'; import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; -import { NewRule, Rule } from '../../../../containers/detection_engine/rules'; +import { Rule } from '../../../../containers/detection_engine/rules'; +import { Type } from '../../../../../../common/detection_engine/schemas/common/schemas'; import { AboutStepRule, @@ -68,7 +68,7 @@ const isThresholdFields = ( fields: QueryRuleFields | MlRuleFields | ThresholdRuleFields ): fields is ThresholdRuleFields => has('threshold', fields); -export const filterRuleFieldsForType = (fields: T, type: RuleType) => { +export const filterRuleFieldsForType = (fields: T, type: Type) => { if (isMlRule(type)) { const { index, queryBar, threshold, ...mlRuleFields } = fields; return mlRuleFields; @@ -119,7 +119,7 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep query: ruleFields.queryBar?.query?.query as string, saved_id: ruleFields.queryBar?.saved_id, ...(ruleType === 'query' && - ruleFields.queryBar?.saved_id && { type: 'saved_query' as RuleType }), + ruleFields.queryBar?.saved_id && { type: 'saved_query' as Type }), }; return { @@ -237,16 +237,19 @@ export const formatActionsStepData = (actionsStepData: ActionsStepRule): Actions }; }; -export const formatRule = ( +// Used to format form data in rule edit and +// create flows so "T" here would likely +// either be CreateRulesSchema or Rule +export const formatRule = ( defineStepData: DefineStepRule, aboutStepData: AboutStepRule, scheduleData: ScheduleStepRule, actionsData: ActionsStepRule, rule?: Rule | null -): NewRule => - deepmerge.all([ +): T => + (deepmerge.all([ formatDefineStepData(defineStepData), formatAboutStepData(aboutStepData, rule?.exceptions_list), formatScheduleStepData(scheduleData), formatActionsStepData(actionsData), - ]) as NewRule; + ]) as unknown) as T; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index d2eb3228cbbf3..22d84c593b415 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -9,7 +9,8 @@ import React, { useCallback, useRef, useState, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import styled, { StyledComponent } from 'styled-components'; -import { usePersistRule } from '../../../../containers/detection_engine/rules'; +import { useCreateRule } from '../../../../containers/detection_engine/rules'; +import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; import { @@ -122,7 +123,7 @@ const CreateRulePageComponent: React.FC = () => { [RuleStep.scheduleRule]: false, [RuleStep.ruleActions]: false, }); - const [{ isLoading, isSaved }, setRule] = usePersistRule(); + const [{ isLoading, isSaved }, setRule] = useCreateRule(); const actionMessageParams = useMemo( () => getActionMessageParams((stepsData.current['define-rule'].data as DefineStepRule)?.ruleType), @@ -159,7 +160,7 @@ const CreateRulePageComponent: React.FC = () => { stepsData.current[RuleStep.scheduleRule].isValid ) { setRule( - formatRule( + formatRule( stepsData.current[RuleStep.defineRule].data as DefineStepRule, stepsData.current[RuleStep.aboutRule].data as AboutStepRule, stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 530222ee19624..b251b2eba10ae 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -17,7 +17,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; -import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules'; +import { useRule, useUpdateRule } from '../../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; import { @@ -51,6 +51,7 @@ import { } from '../types'; import * as i18n from './translations'; import { SecurityPageName } from '../../../../../app/types'; +import { UpdateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; interface StepRuleForm { isValid: boolean; @@ -113,7 +114,7 @@ const EditRulePageComponent: FC = () => { [RuleStep.scheduleRule]: null, [RuleStep.ruleActions]: null, }); - const [{ isLoading, isSaved }, setRule] = usePersistRule(); + const [{ isLoading, isSaved }, setRule] = useUpdateRule(); const [tabHasError, setTabHasError] = useState([]); // eslint-disable-next-line react-hooks/exhaustive-deps const actionMessageParams = useMemo(() => getActionMessageParams(rule?.type), [rule]); @@ -261,7 +262,7 @@ const EditRulePageComponent: FC = () => { if (invalidForms.length === 0 && activeForm != null) { setTabHasError([]); setRule({ - ...formatRule( + ...formatRule( (activeFormId === RuleStep.defineRule ? activeForm.data : myDefineRuleForm.data) as DefineStepRule, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index f9279ce639534..8178f5ae5ba1d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -10,7 +10,7 @@ import memoizeOne from 'memoize-one'; import { useLocation } from 'react-router-dom'; import { ActionVariable } from '../../../../../../triggers_actions_ui/public'; -import { RuleAlertAction, RuleType } from '../../../../../common/detection_engine/types'; +import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; import { Filter } from '../../../../../../../../src/plugins/data/public'; @@ -24,7 +24,10 @@ import { ScheduleStepRule, ActionsStepRule, } from './types'; -import { SeverityMapping } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { + SeverityMapping, + Type, +} from '../../../../../common/detection_engine/schemas/common/schemas'; import { severityOptions } from '../../../components/rules/step_about_rule/data'; export interface GetStepsData { @@ -307,7 +310,7 @@ export const redirectToDetections = ( hasEncryptionKey === false || needsListsConfiguration; -const getRuleSpecificRuleParamKeys = (ruleType: RuleType) => { +const getRuleSpecificRuleParamKeys = (ruleType: Type) => { const queryRuleParams = ['index', 'filters', 'language', 'query', 'saved_id']; if (isMlRule(ruleType)) { @@ -321,7 +324,7 @@ const getRuleSpecificRuleParamKeys = (ruleType: RuleType) => { return queryRuleParams; }; -export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { +export const getActionMessageRuleParams = (ruleType: Type): string[] => { const commonRuleParamsKeys = [ 'id', 'name', @@ -349,23 +352,21 @@ export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { return ruleParamsKeys; }; -export const getActionMessageParams = memoizeOne( - (ruleType: RuleType | undefined): ActionVariable[] => { - if (!ruleType) { - return []; - } - const actionMessageRuleParams = getActionMessageRuleParams(ruleType); - - return [ - { name: 'state.signals_count', description: 'state.signals_count' }, - { name: '{context.results_link}', description: 'context.results_link' }, - ...actionMessageRuleParams.map((param) => { - const extendedParam = `context.rule.${param}`; - return { name: extendedParam, description: extendedParam }; - }), - ]; +export const getActionMessageParams = memoizeOne((ruleType: Type | undefined): ActionVariable[] => { + if (!ruleType) { + return []; } -); + const actionMessageRuleParams = getActionMessageRuleParams(ruleType); + + return [ + { name: 'state.signals_count', description: 'state.signals_count' }, + { name: '{context.results_link}', description: 'context.results_link' }, + ...actionMessageRuleParams.map((param) => { + const extendedParam = `context.rule.${param}`; + return { name: extendedParam, description: extendedParam }; + }), + ]; +}); // typed as null not undefined as the initial state for this value is null. export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean => diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index e36f08703dae5..a7603051add49 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RuleAlertAction, RuleType } from '../../../../../common/detection_engine/types'; +import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { AlertAction } from '../../../../../../alerts/common'; import { Filter } from '../../../../../../../../src/plugins/data/common'; import { FormData, FormHook } from '../../../../shared_imports'; @@ -18,13 +18,15 @@ import { RiskScoreMapping, RuleNameOverride, SeverityMapping, + SortOrder, TimestampOverride, + Type, } from '../../../../../common/detection_engine/schemas/common/schemas'; import { List } from '../../../../../common/detection_engine/schemas/types'; export interface EuiBasicTableSortTypes { field: string; - direction: 'asc' | 'desc'; + direction: SortOrder; } export interface EuiBasicTableOnChange { @@ -102,7 +104,7 @@ export interface DefineStepRule extends StepRuleData { index: string[]; machineLearningJobId: string; queryBar: FieldValueQueryBar; - ruleType: RuleType; + ruleType: Type; timeline: FieldValueTimeline; threshold: FieldValueThreshold; } @@ -134,7 +136,7 @@ export interface DefineStepRuleJson { }; timeline_id?: string; timeline_title?: string; - type: RuleType; + type: Type; } export interface AboutStepRuleJson { diff --git a/x-pack/plugins/security_solution/public/helpers.ts b/x-pack/plugins/security_solution/public/helpers.ts index 53fe185ef9a65..63c3f3ea81d98 100644 --- a/x-pack/plugins/security_solution/public/helpers.ts +++ b/x-pack/plugins/security_solution/public/helpers.ts @@ -6,7 +6,12 @@ import { CoreStart } from '../../../../src/core/public'; import { APP_ID } from '../common/constants'; +import { + FactoryQueryTypes, + StrategyResponseType, +} from '../common/search_strategy/security_solution'; import { SecurityPageName } from './app/types'; +import { InspectResponse } from './types'; export const manageOldSiemRoutes = async (coreStart: CoreStart) => { const { application } = coreStart; @@ -73,3 +78,11 @@ export const manageOldSiemRoutes = async (coreStart: CoreStart) => { break; } }; + +export const getInspectResponse = ( + response: StrategyResponseType, + prevResponse: InspectResponse +): InspectResponse => ({ + dsl: response?.inspect?.dsl ?? prevResponse?.dsl ?? [], + response: response != null ? [JSON.stringify(response, null, 2)] : prevResponse?.response, +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx index e8bd0c58f9ace..8e2b47769adf3 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx @@ -10,8 +10,8 @@ import { has } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { hostsActions, hostsModel, hostsSelectors } from '../../store'; -import { AuthenticationsEdges } from '../../../graphql/types'; +import { AuthenticationsEdges } from '../../../../common/search_strategy/security_solution/hosts/authentications'; + import { State } from '../../../common/store'; import { DragEffects, @@ -24,9 +24,11 @@ import { HostDetailsLink, IPDetailsLink } from '../../../common/components/links import { Columns, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; +import { getRowItemDraggables } from '../../../common/components/tables/helpers'; + +import { hostsActions, hostsModel, hostsSelectors } from '../../store'; import * as i18n from './translations'; -import { getRowItemDraggables } from '../../../common/components/tables/helpers'; const tableType = hostsModel.HostsTableType.authentications; diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts index 84682fd14ac6b..9e60c35b746da 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts @@ -3,11 +3,34 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { SearchResponse } from 'elasticsearch'; +import { HostAuthenticationsStrategyResponse } from '../../../../common/search_strategy/security_solution/hosts/authentications'; -import { AuthenticationsData } from '../../../graphql/types'; - -export const mockData: { Authentications: AuthenticationsData } = { +export const mockData: { Authentications: HostAuthenticationsStrategyResponse } = { Authentications: { + rawResponse: { + aggregations: { + group_by_users: { + buckets: [ + { + key: 'SYSTEM', + doc_count: 4, + failures: { + doc_count: 0, + lastFailure: { hits: { total: 0, max_score: null, hits: [] } }, + hits: { total: 0, max_score: null, hits: [] }, + }, + successes: { + doc_count: 4, + lastSuccess: { hits: { total: 4, max_score: null } }, + }, + }, + ], + doc_count_error_upper_bound: -1, + sum_other_doc_count: 566, + }, + }, + } as SearchResponse, totalCount: 54, edges: [ { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index efd80c5c590ed..5436469409194 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -4,35 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { AbortError } from '../../../../../../../src/plugins/data/common'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { HostsQueries } from '../../../../common/search_strategy/security_solution'; import { + HostAuthenticationsRequestOptions, + HostAuthenticationsStrategyResponse, AuthenticationsEdges, - GetAuthenticationsQuery, PageInfoPaginated, -} from '../../../graphql/types'; -import { inputsModel, State, inputsSelectors } from '../../../common/store'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; + DocValueFields, + SortField, +} from '../../../../common/search_strategy'; +import { ESTermQuery } from '../../../../common/typed_json'; + +import { inputsModel, State } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; -import { - QueryTemplatePaginated, - QueryTemplatePaginatedProps, -} from '../../../common/containers/query_template_paginated'; +import { useKibana } from '../../../common/lib/kibana'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; + import { hostsModel, hostsSelectors } from '../../store'; -import { authenticationsQuery } from './index.gql_query'; + +import * as i18n from './translations'; const ID = 'authenticationQuery'; export interface AuthenticationArgs { authentications: AuthenticationsEdges[]; id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; loading: boolean; loadPage: (newActivePage: number) => void; @@ -41,115 +48,158 @@ export interface AuthenticationArgs { totalCount: number; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: AuthenticationArgs) => React.ReactNode; +interface UseAuthentications { + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + endDate: string; + startDate: string; type: hostsModel.HostsType; } -export interface AuthenticationsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; -} +export const useAuthentications = ({ + docValueFields, + filterQuery, + endDate, + startDate, + type, +}: UseAuthentications): [boolean, AuthenticationArgs] => { + const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); + const { activePage, limit } = useSelector( + (state: State) => getAuthenticationsSelector(state, type), + shallowEqual + ); + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + const [authenticationsRequest, setAuthenticationsRequest] = useState< + HostAuthenticationsRequestOptions + >({ + defaultIndex, + docValueFields: docValueFields ?? [], + factoryQueryType: HostsQueries.authentications, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + sort: {} as SortField, + }); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setAuthenticationsRequest((prevRequest) => { + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); -type AuthenticationsProps = OwnProps & AuthenticationsComponentReduxProps & WithKibanaProps; - -class AuthenticationsComponentQuery extends QueryTemplatePaginated< - AuthenticationsProps, - GetAuthenticationsQuery.Query, - GetAuthenticationsQuery.Variables -> { - public render() { - const { - activePage, - children, - docValueFields, - endDate, - filterQuery, - id = ID, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetAuthenticationsQuery.Variables = { - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - pagination: generateTablePaginationOptions(activePage, limit), - filterQuery: createFilter(filterQuery), - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - inspect: isInspected, - docValueFields: docValueFields ?? [], - }; - return ( - - query={authenticationsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const authentications = getOr([], 'source.Authentications.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + const [authenticationsResponse, setAuthenticationsResponse] = useState({ + authentications: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loading: true, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + totalCount: -1, + }); + + const authenticationsSearch = useCallback( + (request: HostAuthenticationsRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setAuthenticationsResponse((prevResponse) => ({ + ...prevResponse, + authentications: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + notifications.toasts.addWarning(i18n.ERROR_AUTHENTICATIONS); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_AUTHENTICATIONS, + text: msg.message, + }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Authentications: { - ...fetchMoreResult.source.Authentications, - edges: [...fetchMoreResult.source.Authentications.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - authentications, - id, - inspect: getOr(null, 'source.Authentications.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Authentications.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.Authentications.totalCount', data), }); - }} - - ); - } -} + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); -const makeMapStateToProps = () => { - const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getAuthenticationsSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; + useEffect(() => { + setAuthenticationsRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + docValueFields: docValueFields ?? [], + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, defaultIndex, docValueFields, endDate, filterQuery, limit, startDate]); -export const AuthenticationsQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(AuthenticationsComponentQuery); + useEffect(() => { + authenticationsSearch(authenticationsRequest); + }, [authenticationsRequest, authenticationsSearch]); + + return [loading, authenticationsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/authentications/translations.ts new file mode 100644 index 0000000000000..95ff6363a3870 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/translations.ts @@ -0,0 +1,21 @@ +/* + * 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 const ERROR_AUTHENTICATIONS = i18n.translate( + 'xpack.securitySolution.authentications.errorSearchDescription', + { + defaultMessage: `An error has occurred on authentications search`, + } +); + +export const FAIL_AUTHENTICATIONS = i18n.translate( + 'xpack.securitySolution.authentications.failSearchDescription', + { + defaultMessage: `Failed to run search on authentications`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx index 3a93b1ee46e7b..169fe58e9a2cc 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx @@ -68,7 +68,7 @@ export const useFirstLastSeenHost = ({ const searchSubscription$ = data.search .search(request, { strategy: 'securitySolutionSearchStrategy', - signal: abortCtrl.current.signal, + abortSignal: abortCtrl.current.signal, }) .subscribe({ next: (response) => { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index 6e1ebbfd1e7bb..958d62dfe9b6a 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -10,22 +10,25 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { HostsEdges, PageInfoPaginated } from '../../../graphql/types'; import { inputsModel, State } from '../../../common/store'; import { createFilter } from '../../../common/containers/helpers'; import { useKibana } from '../../../common/lib/kibana'; import { hostsModel, hostsSelectors } from '../../store'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; import { + HostsEdges, + PageInfoPaginated, DocValueFields, HostsQueries, HostsRequestOptions, HostsStrategyResponse, -} from '../../../../common/search_strategy/security_solution'; +} from '../../../../common/search_strategy'; import { ESTermQuery } from '../../../../common/typed_json'; import * as i18n from './translations'; import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; const ID = 'hostsQuery'; @@ -34,7 +37,7 @@ export interface HostsArgs { endDate: string; hosts: HostsEdges[]; id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; loadPage: LoadPage; pageInfo: PageInfoPaginated; @@ -129,7 +132,7 @@ export const useAllHost = ({ const searchSubscription$ = data.search .search(request, { strategy: 'securitySolutionSearchStrategy', - signal: abortCtrl.current.signal, + abortSignal: abortCtrl.current.signal, }) .subscribe({ next: (response) => { @@ -139,7 +142,7 @@ export const useAllHost = ({ setHostsResponse((prevResponse) => ({ ...prevResponse, hosts: response.edges, - inspect: response.inspect ?? prevResponse.inspect, + inspect: getInspectResponse(response, prevResponse.inspect), pageInfo: response.pageInfo, refetch: refetch.current, totalCount: response.totalCount, diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/_index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/_index.tsx index f766f068f099f..b28f479634d42 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/_index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/_index.tsx @@ -22,12 +22,14 @@ import { import * as i18n from './translations'; import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../../helpers'; +import { InspectResponse } from '../../../../types'; const ID = 'hostOverviewQuery'; export interface HostOverviewArgs { id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; hostOverview: HostItem; refetch: inputsModel.Refetch; startDate: string; @@ -87,7 +89,7 @@ export const useHostOverview = ({ const searchSubscription$ = data.search .search(request, { strategy: 'securitySolutionSearchStrategy', - signal: abortCtrl.current.signal, + abortSignal: abortCtrl.current.signal, }) .subscribe({ next: (response) => { @@ -97,7 +99,7 @@ export const useHostOverview = ({ setHostOverviewResponse((prevResponse) => ({ ...prevResponse, hostOverview: response.hostOverview, - inspect: response.inspect ?? prevResponse.inspect, + inspect: getInspectResponse(response, prevResponse.inspect), refetch: refetch.current, })); } diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/translations.ts index ada713d135c22..c8cd36e40d6e0 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/translations.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/translations.ts @@ -19,3 +19,17 @@ export const FAIL_ALL_HOST = i18n.translate( defaultMessage: `Failed to run search on all hosts`, } ); + +export const ERROR_HOST_OVERVIEW = i18n.translate( + 'xpack.securitySolution.hostOverview.errorSearchDescription', + { + defaultMessage: `An error has occurred on host overview search`, + } +); + +export const FAIL_HOST_OVERVIEW = i18n.translate( + 'xpack.securitySolution.hostOverview.failSearchDescription', + { + defaultMessage: `Failed to run search on host overview`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx index f8e5b1bed73cd..82f5a97e9e413 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx @@ -4,36 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; -import { compose } from 'redux'; +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { AbortError } from '../../../../../../../src/plugins/data/common'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { - GetUncommonProcessesQuery, - PageInfoPaginated, - UncommonProcessesEdges, -} from '../../../graphql/types'; -import { inputsModel, State, inputsSelectors } from '../../../common/store'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { PageInfoPaginated, UncommonProcessesEdges } from '../../../graphql/types'; +import { inputsModel, State } from '../../../common/store'; +import { useKibana } from '../../../common/lib/kibana'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; -import { - QueryTemplatePaginated, - QueryTemplatePaginatedProps, -} from '../../../common/containers/query_template_paginated'; +import { createFilter } from '../../../common/containers/helpers'; + import { hostsModel, hostsSelectors } from '../../store'; -import { uncommonProcessesQuery } from './index.gql_query'; +import { + HostUncommonProcessesRequestOptions, + HostUncommonProcessesStrategyResponse, +} from '../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; +import { HostsQueries } from '../../../../common/search_strategy/security_solution/hosts'; +import { DocValueFields, SortField } from '../../../../common/search_strategy'; + +import * as i18n from './translations'; +import { ESTermQuery } from '../../../../common/typed_json'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; const ID = 'uncommonProcessesQuery'; export interface UncommonProcessesArgs { id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; - loading: boolean; loadPage: (newActivePage: number) => void; pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; @@ -41,111 +44,164 @@ export interface UncommonProcessesArgs { uncommonProcesses: UncommonProcessesEdges[]; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: UncommonProcessesArgs) => React.ReactNode; +interface UseUncommonProcesses { + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + endDate: string; + skip?: boolean; + startDate: string; type: hostsModel.HostsType; } -type UncommonProcessesProps = OwnProps & PropsFromRedux & WithKibanaProps; - -class UncommonProcessesComponentQuery extends QueryTemplatePaginated< - UncommonProcessesProps, - GetUncommonProcessesQuery.Query, - GetUncommonProcessesQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetUncommonProcessesQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - pagination: generateTablePaginationOptions(activePage, limit), - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, +export const useUncommonProcesses = ({ + docValueFields, + filterQuery, + endDate, + skip = false, + startDate, + type, +}: UseUncommonProcesses): [boolean, UncommonProcessesArgs] => { + const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); + const { activePage, limit } = useSelector((state: State) => + getUncommonProcessesSelector(state, type) + ); + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + const [uncommonProcessesRequest, setUncommonProcessesRequest] = useState< + HostUncommonProcessesRequestOptions + >({ + defaultIndex, + docValueFields: docValueFields ?? [], + factoryQueryType: HostsQueries.uncommonProcesses, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + sort: {} as SortField, + }); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setUncommonProcessesRequest((prevRequest) => { + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); + + const [uncommonProcessesResponse, setUncommonProcessesResponse] = useState( + { + uncommonProcesses: [], + id: ID, + inspect: { + dsl: [], + response: [], }, - }; - return ( - - query={uncommonProcessesQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const uncommonProcesses = getOr([], 'source.UncommonProcesses.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + totalCount: -1, + } + ); + + const uncommonProcessesSearch = useCallback( + (request: HostUncommonProcessesRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search( + request, + { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + } + ) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setUncommonProcessesResponse((prevResponse) => ({ + ...prevResponse, + uncommonProcesses: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + notifications.toasts.addWarning(i18n.ERROR_UNCOMMON_PROCESSES); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_UNCOMMON_PROCESSES, + text: msg.message, + }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - UncommonProcesses: { - ...fetchMoreResult.source.UncommonProcesses, - edges: [...fetchMoreResult.source.UncommonProcesses.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.UncommonProcesses.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.UncommonProcesses.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.UncommonProcesses.totalCount', data), - uncommonProcesses, }); - }} - - ); - } -} + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); -const makeMapStateToProps = () => { - const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getUncommonProcessesSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; + useEffect(() => { + setUncommonProcessesRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + docValueFields: docValueFields ?? [], + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + sort: {} as SortField, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, defaultIndex, docValueFields, endDate, filterQuery, limit, skip, startDate]); -const connector = connect(makeMapStateToProps); + useEffect(() => { + uncommonProcessesSearch(uncommonProcessesRequest); + }, [uncommonProcessesRequest, uncommonProcessesSearch]); -type PropsFromRedux = ConnectedProps; - -export const UncommonProcessesQuery = compose>( - connector, - withKibana -)(UncommonProcessesComponentQuery); + return [loading, uncommonProcessesResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/translations.ts new file mode 100644 index 0000000000000..d563d90dfb262 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/translations.ts @@ -0,0 +1,21 @@ +/* + * 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 const ERROR_UNCOMMON_PROCESSES = i18n.translate( + 'xpack.securitySolution.uncommonProcesses.errorSearchDescription', + { + defaultMessage: `An error has occurred on uncommon processes search`, + } +); + +export const FAIL_UNCOMMON_PROCESSES = i18n.translate( + 'xpack.securitySolution.uncommonProcesses.failSearchDescription', + { + defaultMessage: `Failed to run search on uncommon processes`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 6c8eb9eb04941..084d4b699e8eb 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -8,7 +8,7 @@ import { getOr } from 'lodash/fp'; import React, { useEffect } from 'react'; import { AuthenticationTable } from '../../components/authentications_table'; import { manageQuery } from '../../../common/components/page/manage_query'; -import { AuthenticationsQuery } from '../../containers/authentications'; +import { useAuthentications } from '../../containers/authentications'; import { HostsComponentsQueryProps } from './types'; import { hostsModel } from '../../store'; import { @@ -77,6 +77,11 @@ export const AuthenticationsQueryTabBody = ({ }; }, [deleteQuery]); + const [ + loading, + { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useAuthentications({ docValueFields, endDate, filterQuery, startDate, type }); + return ( <> - - {({ - authentications, - totalCount, - loading, - pageInfo, - loadPage, - id, - inspect, - isInspected, - refetch, - }) => ( - - )} - + /> ); }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx index f1691dbaa04b4..713958f05a3da 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx @@ -6,7 +6,7 @@ import { getOr } from 'lodash/fp'; import React from 'react'; -import { UncommonProcessesQuery } from '../../containers/uncommon_processes'; +import { useUncommonProcesses } from '../../containers/uncommon_processes'; import { HostsComponentsQueryProps } from './types'; import { UncommonProcessTable } from '../../components/uncommon_process_table'; import { manageQuery } from '../../../common/components/page/manage_query'; @@ -15,49 +15,35 @@ const UncommonProcessTableManage = manageQuery(UncommonProcessTable); export const UncommonProcessQueryTabBody = ({ deleteQuery, + docValueFields, endDate, filterQuery, skip, setQuery, startDate, type, -}: HostsComponentsQueryProps) => ( - - {({ - uncommonProcesses, - totalCount, - loading, - pageInfo, - loadPage, - id, - inspect, - isInspected, - refetch, - }) => ( - - )} - -); +}: HostsComponentsQueryProps) => { + const [ + loading, + { uncommonProcesses, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useUncommonProcesses({ docValueFields, endDate, filterQuery, skip, startDate, type }); + return ( + + ); +}; UncommonProcessQueryTabBody.dispalyName = 'UncommonProcessQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts index aa9bef7bb5417..cfde474c6290d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts @@ -90,7 +90,6 @@ const endpointListApiPathHandlerMocks = ({ [INGEST_API_EPM_PACKAGES]: (): GetPackagesResponse => { return { response: epmPackages, - success: true, }; }, @@ -114,7 +113,6 @@ const endpointListApiPathHandlerMocks = ({ return { items: [agentPolicy], total: 10, - success: true, perPage: 10, page: 1, }; @@ -132,7 +130,6 @@ const endpointListApiPathHandlerMocks = ({ page: 1, perPage: 10, total: endpointPackagePolicies?.length, - success: true, }; }, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts index 5ef01c00dbf16..1093aed0608d5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -64,7 +64,6 @@ export const mockPolicyResultList: (options?: { total, page: requestPageIndex, perPage: requestPageSize, - success: true, }; return mock; }; @@ -81,7 +80,6 @@ export const policyListApiPathHandlers = (totalPolicies: number = 1) => { [INGEST_API_EPM_PACKAGES]: (): GetPackagesResponse => { return { response: [generator.generateEpmPackage()], - success: true, }; }, }; diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap index 1127528c776b7..02a8802bfced1 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`NetworkTopCountries Table Component rendering it renders the IP Details NetworkTopCountries table 1`] = ` - { ); - expect(wrapper.find('Connect(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); + expect(wrapper.find('Memo(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); }); test('it renders the IP Details NetworkTopCountries table', () => { const wrapper = shallow( @@ -101,7 +101,7 @@ describe('NetworkTopCountries Table Component', () => { ); - expect(wrapper.find('Connect(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); + expect(wrapper.find('Memo(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx index 93d3f410ddde4..114bca9f59d9c 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx @@ -6,7 +6,7 @@ import { last } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { IIndexPattern } from 'src/plugins/data/public'; @@ -16,8 +16,8 @@ import { FlowTargetSourceDest, NetworkTopCountriesEdges, NetworkTopTablesFields, - NetworkTopTablesSortField, -} from '../../../graphql/types'; + SortField, +} from '../../../../common/search_strategy'; import { State } from '../../../common/store'; import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; @@ -25,7 +25,7 @@ import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/component import { getCountriesColumnsCurated } from './columns'; import * as i18n from './translations'; -interface OwnProps { +interface NetworkTopCountriesTableProps { data: NetworkTopCountriesEdges[]; fakeTotalCount: number; flowTargeted: FlowTargetSourceDest; @@ -39,8 +39,6 @@ interface OwnProps { type: networkModel.NetworkType; } -type NetworkTopCountriesTableProps = OwnProps & PropsFromRedux; - const rowItems: ItemsPerRow[] = [ { text: i18n.ROWS_5, @@ -54,139 +52,133 @@ const rowItems: ItemsPerRow[] = [ export const NetworkTopCountriesTableId = 'networkTopCountries-top-talkers'; -const NetworkTopCountriesTableComponent = React.memo( - ({ - activePage, - data, - fakeTotalCount, - flowTargeted, - id, - indexPattern, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - sort, - totalCount, - type, - updateNetworkTable, - }) => { - let tableType: networkModel.TopCountriesTableType; - const headerTitle: string = +const NetworkTopCountriesTableComponent: React.FC = ({ + data, + fakeTotalCount, + flowTargeted, + id, + indexPattern, + isInspect, + loading, + loadPage, + showMorePagesIndicator, + totalCount, + type, +}) => { + const dispatch = useDispatch(); + const getTopCountriesSelector = networkSelectors.topCountriesSelector(); + const { activePage, limit, sort } = useSelector( + (state: State) => getTopCountriesSelector(state, type, flowTargeted), + shallowEqual + ); + + const headerTitle: string = useMemo( + () => flowTargeted === FlowTargetSourceDest.source ? i18n.SOURCE_COUNTRIES - : i18n.DESTINATION_COUNTRIES; + : i18n.DESTINATION_COUNTRIES, + [flowTargeted] + ); + const tableType: networkModel.TopCountriesTableType = useMemo(() => { if (type === networkModel.NetworkType.page) { - tableType = - flowTargeted === FlowTargetSourceDest.source - ? networkModel.NetworkTableType.topCountriesSource - : networkModel.NetworkTableType.topCountriesDestination; - } else { - tableType = - flowTargeted === FlowTargetSourceDest.source - ? networkModel.IpDetailsTableType.topCountriesSource - : networkModel.IpDetailsTableType.topCountriesDestination; + return flowTargeted === FlowTargetSourceDest.source + ? networkModel.NetworkTableType.topCountriesSource + : networkModel.NetworkTableType.topCountriesDestination; } - const field = - sort.field === NetworkTopTablesFields.bytes_out || - sort.field === NetworkTopTablesFields.bytes_in - ? `node.network.${sort.field}` - : `node.${flowTargeted}.${sort.field}`; - - const updateLimitPagination = useCallback( - (newLimit) => - updateNetworkTable({ + return flowTargeted === FlowTargetSourceDest.source + ? networkModel.IpDetailsTableType.topCountriesSource + : networkModel.IpDetailsTableType.topCountriesDestination; + }, [flowTargeted, type]); + + const field = + sort.field === NetworkTopTablesFields.bytes_out || + sort.field === NetworkTopTablesFields.bytes_in + ? `node.network.${sort.field}` + : `node.${flowTargeted}.${sort.field}`; + + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + networkActions.updateNetworkTable({ networkType: type, tableType, updates: { limit: newLimit }, - }), - [type, updateNetworkTable, tableType] - ); - - const updateActivePage = useCallback( - (newPage) => - updateNetworkTable({ + }) + ), + [dispatch, type, tableType] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + networkActions.updateNetworkTable({ networkType: type, tableType, updates: { activePage: newPage }, - }), - [type, updateNetworkTable, tableType] - ); - - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null) { - const splitField = criteria.sort.field.split('.'); - const lastField = last(splitField); - const newSortDirection = - lastField !== sort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click - const newTopCountriesSort: NetworkTopTablesSortField = { - field: lastField as NetworkTopTablesFields, - direction: newSortDirection as Direction, - }; - if (!deepEqual(newTopCountriesSort, sort)) { - updateNetworkTable({ + }) + ), + [dispatch, type, tableType] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const lastField = last(splitField) as NetworkTopTablesFields; + const newSortDirection = + lastField !== sort.field ? Direction.desc : (criteria.sort.direction as Direction); // sort by desc on init click + const newTopCountriesSort: SortField = { + field: lastField, + direction: newSortDirection, + }; + if (!deepEqual(newTopCountriesSort, sort)) { + dispatch( + networkActions.updateNetworkTable({ networkType: type, tableType, updates: { sort: newTopCountriesSort, }, - }); - } + }) + ); } - }, - [type, sort, tableType, updateNetworkTable] - ); - - const columns = useMemo( - () => - getCountriesColumnsCurated(indexPattern, flowTargeted, type, NetworkTopCountriesTableId), - [indexPattern, flowTargeted, type] - ); - - return ( - - ); - } -); - -NetworkTopCountriesTableComponent.displayName = 'NetworkTopCountriesTableComponent'; - -const makeMapStateToProps = () => { - const getTopCountriesSelector = networkSelectors.topCountriesSelector(); - return (state: State, { type, flowTargeted }: OwnProps) => - getTopCountriesSelector(state, type, flowTargeted); -}; - -const mapDispatchToProps = { - updateNetworkTable: networkActions.updateNetworkTable, + } + }, + [sort, dispatch, type, tableType] + ); + + const columns = useMemo( + () => getCountriesColumnsCurated(indexPattern, flowTargeted, type, NetworkTopCountriesTableId), + [indexPattern, flowTargeted, type] + ); + + return ( + + ); }; -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; +NetworkTopCountriesTableComponent.displayName = 'NetworkTopCountriesTableComponent'; -export const NetworkTopCountriesTable = connector(NetworkTopCountriesTableComponent); +export const NetworkTopCountriesTable = React.memo(NetworkTopCountriesTableComponent); diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/mock.ts b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/mock.ts index cee775c93d66f..eb6843647f74a 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/mock.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NetworkTopCountriesData } from '../../../graphql/types'; +import { NetworkTopCountriesStrategyResponse } from '../../../../common/search_strategy/security_solution/network'; -export const mockData: { NetworkTopCountries: NetworkTopCountriesData } = { +export const mockData: { NetworkTopCountries: NetworkTopCountriesStrategyResponse } = { NetworkTopCountries: { + rawResponse: {} as NetworkTopCountriesStrategyResponse['rawResponse'], totalCount: 524, edges: [ { diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx index 60845d452d69e..d3e8067d1802e 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx @@ -4,38 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; +import { noop } from 'lodash/fp'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; +import { ESTermQuery } from '../../../../common/typed_json'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { - GetNetworkHttpQuery, - NetworkHttpEdges, - NetworkHttpSortField, - PageInfoPaginated, -} from '../../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../../common/store'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { inputsModel, State } from '../../../common/store'; +import { useKibana } from '../../../common/lib/kibana'; +import { createFilter } from '../../../common/containers/helpers'; +import { NetworkHttpEdges, PageInfoPaginated } from '../../../graphql/types'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; -import { - QueryTemplatePaginated, - QueryTemplatePaginatedProps, -} from '../../../common/containers/query_template_paginated'; import { networkModel, networkSelectors } from '../../store'; -import { networkHttpQuery } from './index.gql_query'; +import { + NetworkQueries, + NetworkHttpRequestOptions, + NetworkHttpStrategyResponse, + SortField, +} from '../../../../common/search_strategy'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; +import * as i18n from './translations'; +import { InspectResponse } from '../../../types'; +import { getInspectResponse } from '../../../helpers'; const ID = 'networkHttpQuery'; export interface NetworkHttpArgs { id: string; ip?: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; - loading: boolean; loadPage: (newActivePage: number) => void; networkHttp: NetworkHttpEdges[]; pageInfo: PageInfoPaginated; @@ -43,118 +42,161 @@ export interface NetworkHttpArgs { totalCount: number; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkHttpArgs) => React.ReactNode; +interface UseNetworkHttp { + id?: string; ip?: string; type: networkModel.NetworkType; + filterQuery?: ESTermQuery | string; + endDate: string; + startDate: string; + skip: boolean; } -export interface NetworkHttpComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkHttpSortField; -} +export const useNetworkHttp = ({ + endDate, + filterQuery, + id = ID, + ip, + skip, + startDate, + type, +}: UseNetworkHttp): [boolean, NetworkHttpArgs] => { + const getHttpSelector = networkSelectors.httpSelector(); + const { activePage, limit, sort } = useSelector( + (state: State) => getHttpSelector(state, type), + shallowEqual + ); + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + + const [networkHttpRequest, setHostRequest] = useState({ + defaultIndex, + factoryQueryType: NetworkQueries.http, + filterQuery: createFilter(filterQuery), + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort: sort as SortField, + timerange: { + interval: '12h', + from: startDate ? startDate : '', + to: endDate ? endDate : new Date(Date.now()).toISOString(), + }, + }); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setHostRequest((prevRequest) => { + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); -type NetworkHttpProps = OwnProps & NetworkHttpComponentReduxProps & WithKibanaProps; + const [networkHttpResponse, setNetworkHttpResponse] = useState({ + networkHttp: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + totalCount: -1, + }); -class NetworkHttpComponentQuery extends QueryTemplatePaginated< - NetworkHttpProps, - GetNetworkHttpQuery.Query, - GetNetworkHttpQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - sort, - startDate, - } = this.props; - const variables: GetNetworkHttpQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkHttpQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkHttp = getOr([], `source.NetworkHttp.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + const networkHttpSearch = useCallback( + (request: NetworkHttpRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setNetworkHttpResponse((prevResponse) => ({ + ...prevResponse, + networkHttp: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_NETWORK_HTTP); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_NETWORK_HTTP, + text: msg.message, + }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkHttp: { - ...fetchMoreResult.source.NetworkHttp, - edges: [...fetchMoreResult.source.NetworkHttp.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkHttp.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkHttp, - pageInfo: getOr({}, 'source.NetworkHttp.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkHttp.totalCount', data), }); - }} - - ); - } -} + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); -const makeMapStateToProps = () => { - const getHttpSelector = networkSelectors.httpSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { id = ID, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getHttpSelector(state, type), - isInspected, - }; - }; -}; + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort: sort as SortField, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip]); + + useEffect(() => { + networkHttpSearch(networkHttpRequest); + }, [networkHttpRequest, networkHttpSearch]); -export const NetworkHttpQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(NetworkHttpComponentQuery); + return [loading, networkHttpResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/translations.ts b/x-pack/plugins/security_solution/public/network/containers/network_http/translations.ts new file mode 100644 index 0000000000000..7909a5e48b8c4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/translations.ts @@ -0,0 +1,21 @@ +/* + * 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 const ERROR_NETWORK_HTTP = i18n.translate( + 'xpack.securitySolution.networkHttp.errorSearchDescription', + { + defaultMessage: `An error has occurred on network http search`, + } +); + +export const FAIL_NETWORK_HTTP = i18n.translate( + 'xpack.securitySolution.networkHttp.failSearchDescription', + { + defaultMessage: `Failed to run search on network http`, + } +); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx index b167cba460818..6bed779d49638 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx @@ -4,161 +4,200 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; +import { noop } from 'lodash/fp'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; +import { ESTermQuery } from '../../../../common/typed_json'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { inputsModel, State } from '../../../common/store'; +import { useKibana } from '../../../common/lib/kibana'; +import { createFilter } from '../../../common/containers/helpers'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { networkModel, networkSelectors } from '../../store'; import { FlowTargetSourceDest, - GetNetworkTopCountriesQuery, + NetworkQueries, NetworkTopCountriesEdges, - NetworkTopTablesSortField, + NetworkTopCountriesRequestOptions, + NetworkTopCountriesStrategyResponse, PageInfoPaginated, -} from '../../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../../common/store'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; -import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; -import { - QueryTemplatePaginated, - QueryTemplatePaginatedProps, -} from '../../../common/containers/query_template_paginated'; -import { networkTopCountriesQuery } from './index.gql_query'; -import { networkModel, networkSelectors } from '../../store'; +} from '../../../../common/search_strategy'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; +import * as i18n from './translations'; const ID = 'networkTopCountriesQuery'; export interface NetworkTopCountriesArgs { id: string; - ip?: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; - loading: boolean; loadPage: (newActivePage: number) => void; - networkTopCountries: NetworkTopCountriesEdges[]; pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; + networkTopCountries: NetworkTopCountriesEdges[]; totalCount: number; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkTopCountriesArgs) => React.ReactNode; +interface UseNetworkTopCountries { flowTarget: FlowTargetSourceDest; ip?: string; type: networkModel.NetworkType; + filterQuery?: ESTermQuery | string; + endDate: string; + startDate: string; + skip: boolean; + id?: string; } -export interface NetworkTopCountriesComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkTopTablesSortField; -} +export const useNetworkTopCountries = ({ + endDate, + filterQuery, + flowTarget, + id = ID, + skip, + startDate, + type, +}: UseNetworkTopCountries): [boolean, NetworkTopCountriesArgs] => { + const getTopCountriesSelector = networkSelectors.topCountriesSelector(); + const { activePage, limit, sort } = useSelector( + (state: State) => getTopCountriesSelector(state, type, flowTarget), + shallowEqual + ); + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + + const [networkTopCountriesRequest, setHostRequest] = useState({ + defaultIndex, + factoryQueryType: NetworkQueries.topCountries, + filterQuery: createFilter(filterQuery), + flowTarget, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + timerange: { + interval: '12h', + from: startDate ? startDate : '', + to: endDate ? endDate : new Date(Date.now()).toISOString(), + }, + }); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setHostRequest((prevRequest) => ({ + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + })); + }, + [limit] + ); -type NetworkTopCountriesProps = OwnProps & NetworkTopCountriesComponentReduxProps & WithKibanaProps; + const [networkTopCountriesResponse, setNetworkTopCountriesResponse] = useState< + NetworkTopCountriesArgs + >({ + networkTopCountries: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + totalCount: -1, + }); -class NetworkTopCountriesComponentQuery extends QueryTemplatePaginated< - NetworkTopCountriesProps, - GetNetworkTopCountriesQuery.Query, - GetNetworkTopCountriesQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - flowTarget, - filterQuery, - kibana, - id = `${ID}-${flowTarget}`, - ip, - isInspected, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetNetworkTopCountriesQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkTopCountriesQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkTopCountries = getOr([], `source.NetworkTopCountries.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + const networkTopCountriesSearch = useCallback( + (request: NetworkTopCountriesRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setNetworkTopCountriesResponse((prevResponse) => ({ + ...prevResponse, + networkTopCountries: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_NETWORK_TOP_COUNTRIES); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_NETWORK_TOP_COUNTRIES, + text: msg.message, + }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkTopCountries: { - ...fetchMoreResult.source.NetworkTopCountries, - edges: [...fetchMoreResult.source.NetworkTopCountries.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkTopCountries.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkTopCountries, - pageInfo: getOr({}, 'source.NetworkTopCountries.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkTopCountries.totalCount', data), }); - }} - - ); - } -} + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); -const makeMapStateToProps = () => { - const getTopCountriesSelector = networkSelectors.topCountriesSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTopCountriesSelector(state, type, flowTarget), - isInspected, - }; - }; -}; + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip]); -export const NetworkTopCountriesQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(NetworkTopCountriesComponentQuery); + useEffect(() => { + networkTopCountriesSearch(networkTopCountriesRequest); + }, [networkTopCountriesRequest, networkTopCountriesSearch]); + + return [loading, networkTopCountriesResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/translations.ts b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/translations.ts new file mode 100644 index 0000000000000..ff807ee268adf --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/translations.ts @@ -0,0 +1,21 @@ +/* + * 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 const ERROR_NETWORK_TOP_COUNTRIES = i18n.translate( + 'xpack.securitySolution.networkTopCountries.errorSearchDescription', + { + defaultMessage: `An error has occurred on network top countries search`, + } +); + +export const FAIL_NETWORK_TOP_COUNTRIES = i18n.translate( + 'xpack.securitySolution.networkTopCountries.failSearchDescription', + { + defaultMessage: `Failed to run search on network top countries`, + } +); diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index 77c6446fbb3d0..df02acf208603 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -23,7 +23,9 @@ import { NetworkTlsStrategyResponse, } from '../../../../common/search_strategy/security_solution/network'; import { AbortError } from '../../../../../../../src/plugins/data/common'; + import * as i18n from './translations'; +import { getInspectResponse } from '../../../helpers'; const ID = 'networkTlsQuery'; @@ -123,7 +125,7 @@ export const useNetworkTls = ({ const searchSubscription$ = data.search .search(request, { strategy: 'securitySolutionSearchStrategy', - signal: abortCtrl.current.signal, + abortSignal: abortCtrl.current.signal, }) .subscribe({ next: (response) => { @@ -133,7 +135,7 @@ export const useNetworkTls = ({ setNetworkTlsResponse((prevResponse) => ({ ...prevResponse, tls: response.edges, - inspect: response.inspect ?? prevResponse.inspect, + inspect: getInspectResponse(response, prevResponse.inspect), pageInfo: response.pageInfo, refetch: refetch.current, totalCount: response.totalCount, diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_http_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/network_http_query_table.tsx index 551de698cfa08..1b1b2b5f4f46e 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_http_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/network_http_query_table.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; import { OwnProps } from './types'; -import { NetworkHttpQuery } from '../../containers/network_http'; +import { useNetworkHttp } from '../../containers/network_http'; import { NetworkHttpTable } from '../../components/network_http_table'; const NetworkHttpTableManage = manageQuery(NetworkHttpTable); @@ -21,43 +21,35 @@ export const NetworkHttpQueryTable = ({ skip, startDate, type, -}: OwnProps) => ( - - {({ - id, - inspect, - isInspected, - loading, - loadPage, - networkHttp, - pageInfo, - refetch, - totalCount, - }) => ( - - )} - -); +}: OwnProps) => { + const [ + loading, + { id, inspect, isInspected, loadPage, networkHttp, pageInfo, refetch, totalCount }, + ] = useNetworkHttp({ + endDate, + filterQuery, + ip, + skip, + startDate, + type, + }); + + return ( + + ); +}; NetworkHttpQueryTable.displayName = 'NetworkHttpQueryTable'; diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_countries_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_countries_query_table.tsx index 6bc80ef1a6aae..42ddd3a6bb4a4 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_countries_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_countries_query_table.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkWithIndexComponentsQueryTableProps } from './types'; -import { NetworkTopCountriesQuery } from '../../containers/network_top_countries'; +import { useNetworkTopCountries } from '../../containers/network_top_countries'; import { NetworkTopCountriesTable } from '../../components/network_top_countries_table'; const NetworkTopCountriesTableManage = manageQuery(NetworkTopCountriesTable); @@ -23,46 +23,38 @@ export const NetworkTopCountriesQueryTable = ({ startDate, type, indexPattern, -}: NetworkWithIndexComponentsQueryTableProps) => ( - - {({ - id, - inspect, - isInspected, - loading, - loadPage, - networkTopCountries, - pageInfo, - refetch, - totalCount, - }) => ( - - )} - -); +}: NetworkWithIndexComponentsQueryTableProps) => { + const [ + loading, + { id, inspect, isInspected, loadPage, networkTopCountries, pageInfo, refetch, totalCount }, + ] = useNetworkTopCountries({ + endDate, + flowTarget, + filterQuery, + ip, + skip, + startDate, + type, + }); + + return ( + + ); +}; NetworkTopCountriesQueryTable.displayName = 'NetworkTopCountriesQueryTable'; diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts b/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts index 9691214cc2820..d1ee48a9a5d9e 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts @@ -8,7 +8,10 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { ESTermQuery } from '../../../../common/typed_json'; import { NetworkType } from '../../store/model'; -import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; +import { + FlowTarget, + FlowTargetSourceDest, +} from '../../../../common/search_strategy/security_solution/network'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; export const type = NetworkType.details; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx index 0c569952458e4..1e57ca42257e7 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { getOr } from 'lodash/fp'; import { NetworkTopCountriesTable } from '../../components/network_top_countries_table'; -import { NetworkTopCountriesQuery } from '../../containers/network_top_countries'; +import { useNetworkTopCountries } from '../../containers/network_top_countries'; import { networkModel } from '../../store'; import { manageQuery } from '../../../common/components/page/manage_query'; @@ -24,45 +24,37 @@ export const CountriesQueryTabBody = ({ setQuery, indexPattern, flowTarget, -}: CountriesQueryTabBodyProps) => ( - - {({ - id, - inspect, - isInspected, - loading, - loadPage, - networkTopCountries, - pageInfo, - refetch, - totalCount, - }) => ( - - )} - -); +}: CountriesQueryTabBodyProps) => { + const [ + loading, + { id, inspect, isInspected, loadPage, networkTopCountries, pageInfo, refetch, totalCount }, + ] = useNetworkTopCountries({ + endDate, + flowTarget, + filterQuery, + skip, + startDate, + type: networkModel.NetworkType.page, + }); + + return ( + + ); +}; CountriesQueryTabBody.displayName = 'CountriesQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx index 7e0c4025d6cac..3caff05734c1e 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { getOr } from 'lodash/fp'; import { NetworkHttpTable } from '../../components/network_http_table'; -import { NetworkHttpQuery } from '../../containers/network_http'; +import { useNetworkHttp } from '../../containers/network_http'; import { networkModel } from '../../store'; import { manageQuery } from '../../../common/components/page/manage_query'; @@ -22,42 +22,34 @@ export const HttpQueryTabBody = ({ skip, startDate, setQuery, -}: HttpQueryTabBodyProps) => ( - - {({ - id, - inspect, - isInspected, - loading, - loadPage, - networkHttp, - pageInfo, - refetch, - totalCount, - }) => ( - - )} - -); +}: HttpQueryTabBodyProps) => { + const [ + loading, + { id, inspect, isInspected, loadPage, networkHttp, pageInfo, refetch, totalCount }, + ] = useNetworkHttp({ + endDate, + filterQuery, + skip, + startDate, + type: networkModel.NetworkType.page, + }); + + return ( + + ); +}; HttpQueryTabBody.displayName = 'HttpQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx index 93582088811dc..2da56a30df7c7 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx @@ -8,7 +8,7 @@ import React, { useCallback } from 'react'; import { Route, Switch } from 'react-router-dom'; import { EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { FlowTargetSourceDest } from '../../../graphql/types'; +import { FlowTargetSourceDest } from '../../../../common/search_strategy/security_solution/network'; import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; import { IPsQueryTabBody } from './ips_query_tab_body'; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts index 183c760e40ab1..2ef04d3371c0b 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts @@ -8,7 +8,7 @@ import { ESTermQuery } from '../../../../common/typed_json'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { NavTab } from '../../../common/components/navigation/types'; -import { FlowTargetSourceDest } from '../../../graphql/types'; +import { FlowTargetSourceDest } from '../../../../common/search_strategy/security_solution/network'; import { networkModel } from '../../store'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; diff --git a/x-pack/plugins/security_solution/public/network/store/selectors.ts b/x-pack/plugins/security_solution/public/network/store/selectors.ts index cef8b139402ef..0246305092a32 100644 --- a/x-pack/plugins/security_solution/public/network/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/network/store/selectors.ts @@ -7,7 +7,7 @@ import { createSelector } from 'reselect'; import { get } from 'lodash/fp'; -import { FlowTargetSourceDest } from '../../graphql/types'; +import { FlowTargetSourceDest } from '../../../common/search_strategy/security_solution/network'; import { State } from '../../common/store/types'; import { initialNetworkState } from './reducer'; import { diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts index dee53a624baff..55d52d4ba3252 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -12,7 +12,6 @@ import { ResolverTree, ResolverEntityIndex, } from '../../../common/endpoint/types'; -import { DEFAULT_INDEX_KEY as defaultIndexKey } from '../../../common/constants'; /** * The data access layer for resolver. All communication with the Kibana server is done through this object. This object is provided to Resolver. In tests, a mock data access layer can be used instead. @@ -38,13 +37,6 @@ export function dataAccessLayerFactory( }); }, - /** - * Used to get the default index pattern from the SIEM application. - */ - indexPatterns(): string[] { - return context.services.uiSettings.get(defaultIndexKey); - }, - /** * Used to get the entity_id for an _id. */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts index 43282848dcf9a..631eab18fc014 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts @@ -12,7 +12,7 @@ import { import { mockTreeWithNoProcessEvents } from '../../mocks/resolver_tree'; import { DataAccessLayer } from '../../types'; -type EmptiableRequests = 'relatedEvents' | 'resolverTree' | 'entities' | 'indexPatterns'; +type EmptiableRequests = 'relatedEvents' | 'resolverTree' | 'entities'; interface Metadata { /** @@ -66,15 +66,6 @@ export function emptifyMock( : dataAccessLayer.resolverTree(...args); }, - /** - * Get an array of index patterns that contain events. - */ - indexPatterns(...args): string[] { - return dataShouldBeEmpty.includes('indexPatterns') - ? [] - : dataAccessLayer.indexPatterns(...args); - }, - /** * Get entities matching a document. */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts index b0407fa5d7c1d..0883a3787fcce 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts @@ -78,13 +78,6 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me ); }, - /** - * Get an array of index patterns that contain events. - */ - indexPatterns(): string[] { - return ['index pattern']; - }, - /** * Get entities matching a document. */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts new file mode 100644 index 0000000000000..ec0fa93485783 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts @@ -0,0 +1,97 @@ +/* + * 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 { + ResolverRelatedEvents, + ResolverTree, + ResolverEntityIndex, +} from '../../../../common/endpoint/types'; +import { mockEndpointEvent } from '../../mocks/endpoint_event'; +import { mockTreeWithNoAncestorsAnd2Children } from '../../mocks/resolver_tree'; +import { DataAccessLayer } from '../../types'; + +interface Metadata { + /** + * The `_id` of the document being analyzed. + */ + databaseDocumentID: string; + /** + * A record of entityIDs to be used in tests assertions. + */ + entityIDs: { + /** + * The entityID of the node related to the document being analyzed. + */ + origin: 'origin'; + /** + * The entityID of the first child of the origin. + */ + firstChild: 'firstChild'; + /** + * The entityID of the second child of the origin. + */ + secondChild: 'secondChild'; + }; +} + +/** + * A mock DataAccessLayer that will return an origin in two children. The `entity` response will be empty unless + * `awesome_index` is passed in the indices array. + */ +export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): { + dataAccessLayer: DataAccessLayer; + metadata: Metadata; +} { + const metadata: Metadata = { + databaseDocumentID: '_id', + entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' }, + }; + return { + metadata, + dataAccessLayer: { + /** + * Fetch related events for an entity ID + */ + relatedEvents(entityID: string): Promise { + return Promise.resolve({ + entityID, + events: [ + mockEndpointEvent({ + entityID, + name: 'event', + timestamp: 0, + }), + ], + nextEvent: null, + }); + }, + + /** + * Fetch a ResolverTree for a entityID + */ + resolverTree(): Promise { + return Promise.resolve( + mockTreeWithNoAncestorsAnd2Children({ + originID: metadata.entityIDs.origin, + firstChildID: metadata.entityIDs.firstChild, + secondChildID: metadata.entityIDs.secondChild, + }) + ); + }, + + /** + * Get entities matching a document. + */ + entities({ indices }): Promise { + // Only return values if the `indices` array contains exactly `'awesome_index'` + if (indices.length === 1 && indices[0] === 'awesome_index') { + return Promise.resolve([{ entity_id: metadata.entityIDs.origin }]); + } + return Promise.resolve([]); + }, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts index 01e75e3eefdbf..95ec0cd1a5f77 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts @@ -76,13 +76,6 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): { return Promise.resolve(tree); }, - /** - * Get an array of index patterns that contain events. - */ - indexPatterns(): string[] { - return ['index pattern']; - }, - /** * Get entities matching a document. */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts index baddcdfd0cd84..6a4955b104b8f 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts @@ -105,13 +105,6 @@ export function pausifyMock({ return dataAccessLayer.resolverTree(...args); }, - /** - * Get an array of index patterns that contain events. - */ - indexPatterns(...args): string[] { - return dataAccessLayer.indexPatterns(...args); - }, - /** * Get entities matching a document. */ diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts b/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts new file mode 100644 index 0000000000000..98efb459a0691 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts @@ -0,0 +1,17 @@ +/* + * 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 { TreeFetcherParameters } from '../types'; + +/** + * A factory for the most basic `TreeFetcherParameters`. Many tests need to provide this even when the values aren't relevant to the test. + */ +export function mockTreeFetcherParameters(): TreeFetcherParameters { + return { + databaseDocumentID: '', + indices: [], + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.test.ts b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.test.ts new file mode 100644 index 0000000000000..faa4edfccdc35 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.test.ts @@ -0,0 +1,38 @@ +/* + * 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 { TreeFetcherParameters } from '../types'; + +import { equal } from './tree_fetcher_parameters'; +describe('TreeFetcherParameters#equal:', () => { + const cases: Array<[TreeFetcherParameters, TreeFetcherParameters, boolean]> = [ + // different databaseDocumentID + [{ databaseDocumentID: 'a', indices: [] }, { databaseDocumentID: 'b', indices: [] }, false], + // different indices length + [{ databaseDocumentID: 'a', indices: [''] }, { databaseDocumentID: 'a', indices: [] }, false], + // same indices length, different databaseDocumentID + [{ databaseDocumentID: 'a', indices: [''] }, { databaseDocumentID: 'b', indices: [''] }, false], + // 1 item in `indices` + [{ databaseDocumentID: 'b', indices: [''] }, { databaseDocumentID: 'b', indices: [''] }, true], + // 2 item in `indices` + [ + { databaseDocumentID: 'b', indices: ['1', '2'] }, + { databaseDocumentID: 'b', indices: ['1', '2'] }, + true, + ], + // 2 item in `indices`, but order inversed + [ + { databaseDocumentID: 'b', indices: ['2', '1'] }, + { databaseDocumentID: 'b', indices: ['1', '2'] }, + true, + ], + ]; + describe.each(cases)('%p when compared to %p', (first, second, expected) => { + it(`should ${expected ? '' : 'not'}be equal`, () => { + expect(equal(first, second)).toBe(expected); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts new file mode 100644 index 0000000000000..d8280c7490901 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts @@ -0,0 +1,40 @@ +/* + * 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 { TreeFetcherParameters } from '../types'; + +/** + * Determine if two instances of `TreeFetcherParameters` are equivalent. Use this to determine if + * a change to a `TreeFetcherParameters` warrants invaliding a request or response. + */ +export function equal(param1: TreeFetcherParameters, param2?: TreeFetcherParameters): boolean { + if (!param2) { + return false; + } + if (param1 === param2) { + return true; + } + if (param1.databaseDocumentID !== param2.databaseDocumentID) { + return false; + } + return arraysContainTheSameElements(param1.indices, param2.indices); +} + +function arraysContainTheSameElements(first: unknown[], second: unknown[]): boolean { + if (first === second) { + return true; + } + if (first.length !== second.length) { + return false; + } + const firstSet = new Set(first); + for (let index = 0; index < second.length; index++) { + if (!firstSet.has(second[index])) { + return false; + } + } + return true; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts index e03f24d78e2a2..7d71cbd97b9ee 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts @@ -115,7 +115,7 @@ interface AppReceivedNewExternalProperties { /** * the `_id` of an ES document. This defines the origin of the Resolver graph. */ - databaseDocumentID?: string; + databaseDocumentID: string; /** * An ID that uniquely identifies this Resolver instance from other concurrent Resolvers. */ @@ -125,6 +125,11 @@ interface AppReceivedNewExternalProperties { * The `search` part of the URL of this page. */ locationSearch: string; + + /** + * Indices that the backend will use to find the document. + */ + indices: string[]; }; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index 466c37d4ad5f1..59d1494ae8c27 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -5,6 +5,7 @@ */ import { ResolverRelatedEvents, ResolverTree } from '../../../../common/endpoint/types'; +import { TreeFetcherParameters } from '../../types'; interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; @@ -14,9 +15,9 @@ interface ServerReturnedResolverData { */ result: ResolverTree; /** - * The database document ID that was used to fetch the resolver tree + * The database parameters that was used to fetch the resolver tree */ - databaseDocumentID: string; + parameters: TreeFetcherParameters; }; } @@ -25,7 +26,7 @@ interface AppRequestedResolverData { /** * entity ID used to make the request. */ - readonly payload: string; + readonly payload: TreeFetcherParameters; } interface ServerFailedToReturnResolverData { @@ -33,7 +34,7 @@ interface ServerFailedToReturnResolverData { /** * entity ID used to make the failed request */ - readonly payload: string; + readonly payload: TreeFetcherParameters; } interface AppAbortedResolverDataRequest { @@ -41,7 +42,7 @@ interface AppAbortedResolverDataRequest { /** * entity ID used to make the aborted request */ - readonly payload: string; + readonly payload: TreeFetcherParameters; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts index e087db9f74685..e6e525334e818 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts @@ -12,6 +12,7 @@ import { DataState } from '../../types'; import { DataAction } from './action'; import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types'; import * as eventModel from '../../../../common/endpoint/models/event'; +import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; /** * Test the data reducer and selector. @@ -27,7 +28,7 @@ describe('Resolver Data Middleware', () => { type: 'serverReturnedResolverData', payload: { result: tree, - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }; store.dispatch(action); @@ -59,6 +60,7 @@ describe('Resolver Data Middleware', () => { let firstChildNodeInTree: TreeNode; let eventStatsForFirstChildNode: { total: number; byCategory: Record }; let categoryToOverCount: string; + let aggregateCategoryTotalForFirstChildNode: number; let tree: ResolverTree; /** @@ -73,6 +75,7 @@ describe('Resolver Data Middleware', () => { firstChildNodeInTree, eventStatsForFirstChildNode, categoryToOverCount, + aggregateCategoryTotalForFirstChildNode, } = mockedTree()); if (tree) { dispatchTree(tree); @@ -138,6 +141,13 @@ describe('Resolver Data Middleware', () => { expect(notDisplayed(typeCounted)).toBe(0); } }); + it('should return an overall correct count for the number of related events', () => { + const aggregateTotalByEntityId = selectors.relatedEventAggregateTotalByEntityId( + store.getState() + ); + const countForId = aggregateTotalByEntityId(firstChildNodeInTree.id); + expect(countForId).toBe(aggregateCategoryTotalForFirstChildNode); + }); }); describe('when data was received and stats show more related events than the API can provide', () => { beforeEach(() => { @@ -262,6 +272,7 @@ function mockedTree() { tree: tree!, firstChildNodeInTree, eventStatsForFirstChildNode: statsResults.eventStats, + aggregateCategoryTotalForFirstChildNode: statsResults.aggregateCategoryTotal, categoryToOverCount: statsResults.firstCategory, }; } @@ -288,6 +299,7 @@ function compileStatsForChild( }; /** The category of the first event. */ firstCategory: string; + aggregateCategoryTotal: number; } { const totalRelatedEvents = node.relatedEvents.length; // For the purposes of testing, we pick one category to fake an extra event for @@ -295,6 +307,12 @@ function compileStatsForChild( let firstCategory: string | undefined; + // This is the "aggregate total" which is displayed to users as the total count + // of related events for the node. It is tallied by incrementing for every discrete + // event.category in an event.category array (or just 1 for a plain string). E.g. two events + // categories 'file' and ['dns','network'] would have an `aggregate total` of 3. + let aggregateCategoryTotal: number = 0; + const compiledStats = node.relatedEvents.reduce( (counts: Record, relatedEvent) => { // `relatedEvent.event.category` is `string | string[]`. @@ -310,6 +328,7 @@ function compileStatsForChild( // Increment the count of events with this category counts[category] = counts[category] ? counts[category] + 1 : 1; + aggregateCategoryTotal++; } return counts; }, @@ -327,5 +346,6 @@ function compileStatsForChild( byCategory: compiledStats, }, firstCategory, + aggregateCategoryTotal, }; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index c43182ddbf835..c8df95aaee6f4 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -7,34 +7,53 @@ import { Reducer } from 'redux'; import { DataState } from '../../types'; import { ResolverAction } from '../actions'; +import * as treeFetcherParameters from '../../models/tree_fetcher_parameters'; const initialState: DataState = { relatedEvents: new Map(), relatedEventsReady: new Map(), resolverComponentInstanceID: undefined, + tree: {}, }; export const dataReducer: Reducer = (state = initialState, action) => { if (action.type === 'appReceivedNewExternalProperties') { const nextState: DataState = { ...state, - databaseDocumentID: action.payload.databaseDocumentID, + tree: { + ...state.tree, + currentParameters: { + databaseDocumentID: action.payload.databaseDocumentID, + indices: action.payload.indices, + }, + }, resolverComponentInstanceID: action.payload.resolverComponentInstanceID, }; return nextState; } else if (action.type === 'appRequestedResolverData') { // keep track of what we're requesting, this way we know when to request and when not to. - return { + const nextState: DataState = { ...state, - pendingRequestDatabaseDocumentID: action.payload, + tree: { + ...state.tree, + pendingRequestParameters: { + databaseDocumentID: action.payload.databaseDocumentID, + indices: action.payload.indices, + }, + }, }; + return nextState; } else if (action.type === 'appAbortedResolverDataRequest') { - if (action.payload === state.pendingRequestDatabaseDocumentID) { + if (treeFetcherParameters.equal(action.payload, state.tree.pendingRequestParameters)) { // the request we were awaiting was aborted - return { + const nextState: DataState = { ...state, - pendingRequestDatabaseDocumentID: undefined, + tree: { + ...state.tree, + pendingRequestParameters: undefined, + }, }; + return nextState; } else { return state; } @@ -43,29 +62,35 @@ export const dataReducer: Reducer = (state = initialS const nextState: DataState = { ...state, - /** - * Store the last received data, as well as the databaseDocumentID it relates to. - */ - lastResponse: { - result: action.payload.result, - databaseDocumentID: action.payload.databaseDocumentID, - successful: true, - }, + tree: { + ...state.tree, + /** + * Store the last received data, as well as the databaseDocumentID it relates to. + */ + lastResponse: { + result: action.payload.result, + parameters: action.payload.parameters, + successful: true, + }, - // This assumes that if we just received something, there is no longer a pending request. - // This cannot model multiple in-flight requests - pendingRequestDatabaseDocumentID: undefined, + // This assumes that if we just received something, there is no longer a pending request. + // This cannot model multiple in-flight requests + pendingRequestParameters: undefined, + }, }; return nextState; } else if (action.type === 'serverFailedToReturnResolverData') { /** Only handle this if we are expecting a response */ - if (state.pendingRequestDatabaseDocumentID !== undefined) { + if (state.tree.pendingRequestParameters !== undefined) { const nextState: DataState = { ...state, - pendingRequestDatabaseDocumentID: undefined, - lastResponse: { - databaseDocumentID: state.pendingRequestDatabaseDocumentID, - successful: false, + tree: { + ...state.tree, + pendingRequestParameters: undefined, + lastResponse: { + parameters: state.tree.pendingRequestParameters, + successful: false, + }, }, }; return nextState; @@ -76,16 +101,18 @@ export const dataReducer: Reducer = (state = initialS action.type === 'userRequestedRelatedEventData' || action.type === 'appDetectedMissingEventData' ) { - return { + const nextState: DataState = { ...state, relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload, false]]), }; + return nextState; } else if (action.type === 'serverReturnedRelatedEventData') { - return { + const nextState: DataState = { ...state, relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload.entityID, true]]), relatedEvents: new Map([...state.relatedEvents, [action.payload.entityID, action.payload]]), }; + return nextState; } else { return state; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts index dc478ede72790..539325faffdf0 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts @@ -18,6 +18,7 @@ import { } from '../../mocks/resolver_tree'; import { uniquePidForProcess } from '../../models/process_event'; import { EndpointEvent } from '../../../../common/endpoint/types'; +import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; describe('data state', () => { let actions: ResolverAction[] = []; @@ -39,29 +40,32 @@ describe('data state', () => { */ const viewAsAString = (dataState: DataState) => { return [ - ['is loading', selectors.isLoading(dataState)], - ['has an error', selectors.hasError(dataState)], + ['is loading', selectors.isTreeLoading(dataState)], + ['has an error', selectors.hadErrorLoadingTree(dataState)], ['has more children', selectors.hasMoreChildren(dataState)], ['has more ancestors', selectors.hasMoreAncestors(dataState)], - ['document to fetch', selectors.databaseDocumentIDToFetch(dataState)], - ['requires a pending request to be aborted', selectors.databaseDocumentIDToAbort(dataState)], + ['parameters to fetch', selectors.treeParametersToFetch(dataState)], + [ + 'requires a pending request to be aborted', + selectors.treeRequestParametersToAbort(dataState), + ], ] .map(([message, value]) => `${message}: ${JSON.stringify(value)}`) .join('\n'); }; - it(`shouldn't initially be loading, or have an error, or have more children or ancestors, or have a document to fetch, or have a pending request that needs to be aborted.`, () => { + it(`shouldn't initially be loading, or have an error, or have more children or ancestors, or have a request to make, or have a pending request that needs to be aborted.`, () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` "is loading: false has an error: false has more children: false has more ancestors: false - document to fetch: null + parameters to fetch: null requires a pending request to be aborted: null" `); }); - describe('when there is a databaseDocumentID but no pending request', () => { + describe('when there are parameters to fetch but no pending request', () => { const databaseDocumentID = 'databaseDocumentID'; const resolverComponentInstanceID = 'resolverComponentInstanceID'; beforeEach(() => { @@ -74,12 +78,13 @@ describe('data state', () => { // `locationSearch` doesn't matter for this test locationSearch: '', + indices: [], }, }, ]; }); - it('should need to fetch the databaseDocumentID', () => { - expect(selectors.databaseDocumentIDToFetch(state())).toBe(databaseDocumentID); + it('should need to request the tree', () => { + expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe(databaseDocumentID); }); it('should not be loading, have an error, have more children or ancestors, or have a pending request that needs to be aborted.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` @@ -87,39 +92,41 @@ describe('data state', () => { has an error: false has more children: false has more ancestors: false - document to fetch: \\"databaseDocumentID\\" + parameters to fetch: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[]} requires a pending request to be aborted: null" `); }); }); - describe('when there is a pending request but no databaseDocumentID', () => { + describe('when there is a pending request but no current tree fetching parameters', () => { const databaseDocumentID = 'databaseDocumentID'; beforeEach(() => { actions = [ { type: 'appRequestedResolverData', - payload: databaseDocumentID, + payload: { databaseDocumentID, indices: [] }, }, ]; }); it('should be loading', () => { - expect(selectors.isLoading(state())).toBe(true); + expect(selectors.isTreeLoading(state())).toBe(true); }); it('should have a request to abort', () => { - expect(selectors.databaseDocumentIDToAbort(state())).toBe(databaseDocumentID); + expect(selectors.treeRequestParametersToAbort(state())?.databaseDocumentID).toBe( + databaseDocumentID + ); }); - it('should not have an error, more children, more ancestors, or a document to fetch.', () => { + it('should not have an error, more children, more ancestors, or request to make.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` "is loading: true has an error: false has more children: false has more ancestors: false - document to fetch: null - requires a pending request to be aborted: \\"databaseDocumentID\\"" + parameters to fetch: null + requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[]}" `); }); }); - describe('when there is a pending request for the current databaseDocumentID', () => { + describe('when there is a pending request that was made using the current parameters', () => { const databaseDocumentID = 'databaseDocumentID'; const resolverComponentInstanceID = 'resolverComponentInstanceID'; beforeEach(() => { @@ -132,27 +139,28 @@ describe('data state', () => { // `locationSearch` doesn't matter for this test locationSearch: '', + indices: [], }, }, { type: 'appRequestedResolverData', - payload: databaseDocumentID, + payload: { databaseDocumentID, indices: [] }, }, ]; }); it('should be loading', () => { - expect(selectors.isLoading(state())).toBe(true); + expect(selectors.isTreeLoading(state())).toBe(true); }); it('should not have a request to abort', () => { - expect(selectors.databaseDocumentIDToAbort(state())).toBe(null); + expect(selectors.treeRequestParametersToAbort(state())).toBe(null); }); - it('should not have an error, more children, more ancestors, a document to begin fetching, or a pending request that should be aborted.', () => { + it('should not have an error, more children, more ancestors, a request to make, or a pending request that should be aborted.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` "is loading: true has an error: false has more children: false has more ancestors: false - document to fetch: null + parameters to fetch: null requires a pending request to be aborted: null" `); }); @@ -160,28 +168,28 @@ describe('data state', () => { beforeEach(() => { actions.push({ type: 'serverFailedToReturnResolverData', - payload: databaseDocumentID, + payload: { databaseDocumentID, indices: [] }, }); }); it('should not be loading', () => { - expect(selectors.isLoading(state())).toBe(false); + expect(selectors.isTreeLoading(state())).toBe(false); }); it('should have an error', () => { - expect(selectors.hasError(state())).toBe(true); + expect(selectors.hadErrorLoadingTree(state())).toBe(true); }); - it('should not be loading, have more children, have more ancestors, have a document to fetch, or have a pending request that needs to be aborted.', () => { + it('should not be loading, have more children, have more ancestors, have a request to make, or have a pending request that needs to be aborted.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` "is loading: false has an error: true has more children: false has more ancestors: false - document to fetch: null + parameters to fetch: null requires a pending request to be aborted: null" `); }); }); }); - describe('when there is a pending request for a different databaseDocumentID than the current one', () => { + describe('when there is a pending request that was made with parameters that are different than the current tree fetching parameters', () => { const firstDatabaseDocumentID = 'first databaseDocumentID'; const secondDatabaseDocumentID = 'second databaseDocumentID'; const resolverComponentInstanceID1 = 'resolverComponentInstanceID1'; @@ -196,12 +204,13 @@ describe('data state', () => { resolverComponentInstanceID: resolverComponentInstanceID1, // `locationSearch` doesn't matter for this test locationSearch: '', + indices: [], }, }, // this happens when the middleware starts the request { type: 'appRequestedResolverData', - payload: firstDatabaseDocumentID, + payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [] }, }, // receive a different databaseDocumentID. this should cause the middleware to abort the existing request and start a new one { @@ -211,18 +220,23 @@ describe('data state', () => { resolverComponentInstanceID: resolverComponentInstanceID2, // `locationSearch` doesn't matter for this test locationSearch: '', + indices: [], }, }, ]; }); it('should be loading', () => { - expect(selectors.isLoading(state())).toBe(true); + expect(selectors.isTreeLoading(state())).toBe(true); }); - it('should need to fetch the second databaseDocumentID', () => { - expect(selectors.databaseDocumentIDToFetch(state())).toBe(secondDatabaseDocumentID); + it('should need to request the tree using the second set of parameters', () => { + expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe( + secondDatabaseDocumentID + ); }); it('should need to abort the request for the databaseDocumentID', () => { - expect(selectors.databaseDocumentIDToFetch(state())).toBe(secondDatabaseDocumentID); + expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe( + secondDatabaseDocumentID + ); }); it('should use the correct location for the second resolver', () => { expect(selectors.resolverComponentInstanceID(state())).toBe(resolverComponentInstanceID2); @@ -233,25 +247,27 @@ describe('data state', () => { has an error: false has more children: false has more ancestors: false - document to fetch: \\"second databaseDocumentID\\" - requires a pending request to be aborted: \\"first databaseDocumentID\\"" + parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[]} + requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"first databaseDocumentID\\",\\"indices\\":[]}" `); }); describe('and when the old request was aborted', () => { beforeEach(() => { actions.push({ type: 'appAbortedResolverDataRequest', - payload: firstDatabaseDocumentID, + payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [] }, }); }); it('should not require a pending request to be aborted', () => { - expect(selectors.databaseDocumentIDToAbort(state())).toBe(null); + expect(selectors.treeRequestParametersToAbort(state())).toBe(null); }); it('should have a document to fetch', () => { - expect(selectors.databaseDocumentIDToFetch(state())).toBe(secondDatabaseDocumentID); + expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe( + secondDatabaseDocumentID + ); }); it('should not be loading', () => { - expect(selectors.isLoading(state())).toBe(false); + expect(selectors.isTreeLoading(state())).toBe(false); }); it('should not have an error, more children, or more ancestors.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` @@ -259,7 +275,7 @@ describe('data state', () => { has an error: false has more children: false has more ancestors: false - document to fetch: \\"second databaseDocumentID\\" + parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[]} requires a pending request to be aborted: null" `); }); @@ -267,14 +283,14 @@ describe('data state', () => { beforeEach(() => { actions.push({ type: 'appRequestedResolverData', - payload: secondDatabaseDocumentID, + payload: { databaseDocumentID: secondDatabaseDocumentID, indices: [] }, }); }); it('should not have a document ID to fetch', () => { - expect(selectors.databaseDocumentIDToFetch(state())).toBe(null); + expect(selectors.treeParametersToFetch(state())).toBe(null); }); it('should be loading', () => { - expect(selectors.isLoading(state())).toBe(true); + expect(selectors.isTreeLoading(state())).toBe(true); }); it('should not have an error, more children, more ancestors, or a pending request that needs to be aborted.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` @@ -282,7 +298,7 @@ describe('data state', () => { has an error: false has more children: false has more ancestors: false - document to fetch: null + parameters to fetch: null requires a pending request to be aborted: null" `); }); @@ -303,7 +319,7 @@ describe('data state', () => { secondAncestorID, }), // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); @@ -331,7 +347,7 @@ describe('data state', () => { secondAncestorID, }), // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); @@ -355,7 +371,7 @@ describe('data state', () => { payload: { result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }), // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); @@ -386,7 +402,7 @@ describe('data state', () => { payload: { result: tree, // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); @@ -417,7 +433,7 @@ describe('data state', () => { payload: { result: tree, // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); @@ -433,7 +449,7 @@ describe('data state', () => { payload: { result: tree, // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 965547f1e309a..e647828ddb606 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -15,6 +15,7 @@ import { AABB, VisibleEntites, SectionData, + TreeFetcherParameters, } from '../../types'; import { isGraphableProcess, @@ -34,6 +35,7 @@ import { LegacyEndpointEvent, } from '../../../../common/endpoint/types'; import * as resolverTreeModel from '../../models/resolver_tree'; +import * as treeFetcherParametersModel from '../../models/tree_fetcher_parameters'; import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout'; import * as eventModel from '../../../../common/endpoint/models/event'; import * as vector2 from '../../models/vector2'; @@ -42,26 +44,25 @@ import { formatDate } from '../../view/panels/panel_content_utilities'; /** * If there is currently a request. */ -export function isLoading(state: DataState): boolean { - return state.pendingRequestDatabaseDocumentID !== undefined; +export function isTreeLoading(state: DataState): boolean { + return state.tree.pendingRequestParameters !== undefined; } /** - * A string for uniquely identifying the instance of resolver within the app. + * If a request was made and it threw an error or returned a failure response code. */ -export function resolverComponentInstanceID(state: DataState): string { - return state.resolverComponentInstanceID ? state.resolverComponentInstanceID : ''; +export function hadErrorLoadingTree(state: DataState): boolean { + if (state.tree.lastResponse) { + return !state.tree.lastResponse.successful; + } + return false; } /** - * If a request was made and it threw an error or returned a failure response code. + * A string for uniquely identifying the instance of resolver within the app. */ -export function hasError(state: DataState): boolean { - if (state.lastResponse && state.lastResponse.successful === false) { - return true; - } else { - return false; - } +export function resolverComponentInstanceID(state: DataState): string { + return state.resolverComponentInstanceID ? state.resolverComponentInstanceID : ''; } /** @@ -69,11 +70,7 @@ export function hasError(state: DataState): boolean { * we're currently interested in. */ const resolverTreeResponse = (state: DataState): ResolverTree | undefined => { - if (state.lastResponse && state.lastResponse.successful) { - return state.lastResponse.result; - } else { - return undefined; - } + return state.tree.lastResponse?.successful ? state.tree.lastResponse.result : undefined; }; /** @@ -170,6 +167,26 @@ export const relatedEventsStats: ( } ); +/** + * This returns the "aggregate total" for related events, tallied as the sum + * of their individual `event.category`s. E.g. a [DNS, Network] would count as two + * towards the aggregate total. + */ +export const relatedEventAggregateTotalByEntityId: ( + state: DataState +) => (entityId: string) => number = createSelector(relatedEventsStats, (relatedStats) => { + return (entityId) => { + const statsForEntity = relatedStats(entityId); + if (statsForEntity === undefined) { + return 0; + } + return Object.values(statsForEntity?.events?.byCategory || {}).reduce( + (sum, val) => sum + val, + 0 + ); + }; +}); + /** * returns a map of entity_ids to related event data. */ @@ -438,18 +455,24 @@ export const relatedEventInfoByEntityId: ( ); /** - * If we need to fetch, this is the ID to fetch. + * If the tree resource needs to be fetched then these are the parameters that should be used. */ -export function databaseDocumentIDToFetch(state: DataState): string | null { - // If there is an ID, it must match either the last received version, or the pending version. - // Otherwise, we need to fetch it - // NB: this technique will not allow for refreshing of data. +export function treeParametersToFetch(state: DataState): TreeFetcherParameters | null { + /** + * If there are current tree parameters that don't match the parameters used in the pending request (if there is a pending request) and that don't match the parameters used in the last completed request (if there was a last completed request) then we need to fetch the tree resource using the current parameters. + */ if ( - state.databaseDocumentID !== undefined && - state.databaseDocumentID !== state.pendingRequestDatabaseDocumentID && - state.databaseDocumentID !== state.lastResponse?.databaseDocumentID + state.tree.currentParameters !== undefined && + !treeFetcherParametersModel.equal( + state.tree.currentParameters, + state.tree.lastResponse?.parameters + ) && + !treeFetcherParametersModel.equal( + state.tree.currentParameters, + state.tree.pendingRequestParameters + ) ) { - return state.databaseDocumentID; + return state.tree.currentParameters; } else { return null; } @@ -672,15 +695,18 @@ export const nodesAndEdgelines: ( /** * If there is a pending request that's for a entity ID that doesn't matche the `entityID`, then we should cancel it. */ -export function databaseDocumentIDToAbort(state: DataState): string | null { +export function treeRequestParametersToAbort(state: DataState): TreeFetcherParameters | null { /** - * If there is a pending request, and its not for the current databaseDocumentID (even, if the current databaseDocumentID is undefined) then we should abort the request. + * If there is a pending request, and its not for the current parameters (even, if the current parameters are undefined) then we should abort the request. */ if ( - state.pendingRequestDatabaseDocumentID !== undefined && - state.pendingRequestDatabaseDocumentID !== state.databaseDocumentID + state.tree.pendingRequestParameters !== undefined && + !treeFetcherParametersModel.equal( + state.tree.pendingRequestParameters, + state.tree.currentParameters + ) ) { - return state.pendingRequestDatabaseDocumentID; + return state.tree.pendingRequestParameters; } else { return null; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts index e91c455c9445f..28948debae891 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts @@ -12,6 +12,7 @@ import { LegacyEndpointEvent, ResolverEvent } from '../../../../common/endpoint/ import { visibleNodesAndEdgeLines } from '../selectors'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; import { mock as mockResolverTree } from '../../models/resolver_tree'; +import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; describe('resolver visible entities', () => { let processA: LegacyEndpointEvent; @@ -112,7 +113,7 @@ describe('resolver visible entities', () => { ]; const action: ResolverAction = { type: 'serverReturnedResolverData', - payload: { result: mockResolverTree({ events })!, databaseDocumentID: '' }, + payload: { result: mockResolverTree({ events })!, parameters: mockTreeFetcherParameters() }, }; const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [300, 200] }; store.dispatch(action); @@ -140,7 +141,7 @@ describe('resolver visible entities', () => { ]; const action: ResolverAction = { type: 'serverReturnedResolverData', - payload: { result: mockResolverTree({ events })!, databaseDocumentID: '' }, + payload: { result: mockResolverTree({ events })!, parameters: mockTreeFetcherParameters() }, }; const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [2000, 2000] }; store.dispatch(action); diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts index 0ec340efbdac9..ef4ca2380ebf4 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts @@ -28,32 +28,31 @@ export function ResolverTreeFetcher( // if the entityID changes while return async () => { const state = api.getState(); - const databaseDocumentIDToFetch = selectors.databaseDocumentIDToFetch(state); + const databaseParameters = selectors.treeParametersToFetch(state); - if (selectors.databaseDocumentIDToAbort(state) && lastRequestAbortController) { + if (selectors.treeRequestParametersToAbort(state) && lastRequestAbortController) { lastRequestAbortController.abort(); // calling abort will cause an action to be fired - } else if (databaseDocumentIDToFetch !== null) { + } else if (databaseParameters !== null) { lastRequestAbortController = new AbortController(); let result: ResolverTree | undefined; // Inform the state that we've made the request. Without this, the middleware will try to make the request again // immediately. api.dispatch({ type: 'appRequestedResolverData', - payload: databaseDocumentIDToFetch, + payload: databaseParameters, }); try { - const indices: string[] = dataAccessLayer.indexPatterns(); const matchingEntities: ResolverEntityIndex = await dataAccessLayer.entities({ - _id: databaseDocumentIDToFetch, - indices, + _id: databaseParameters.databaseDocumentID, + indices: databaseParameters.indices ?? [], signal: lastRequestAbortController.signal, }); if (matchingEntities.length < 1) { // If no entity_id could be found for the _id, bail out with a failure. api.dispatch({ type: 'serverFailedToReturnResolverData', - payload: databaseDocumentIDToFetch, + payload: databaseParameters, }); return; } @@ -67,12 +66,12 @@ export function ResolverTreeFetcher( if (error instanceof DOMException && error.name === 'AbortError') { api.dispatch({ type: 'appAbortedResolverDataRequest', - payload: databaseDocumentIDToFetch, + payload: databaseParameters, }); } else { api.dispatch({ type: 'serverFailedToReturnResolverData', - payload: databaseDocumentIDToFetch, + payload: databaseParameters, }); } } @@ -81,7 +80,7 @@ export function ResolverTreeFetcher( type: 'serverReturnedResolverData', payload: { result, - databaseDocumentID: databaseDocumentIDToFetch, + parameters: databaseParameters, }, }); } diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts index f113e861d3ce9..d15274f0363ac 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts @@ -14,6 +14,7 @@ import { mockTreeWithNoAncestorsAnd2Children, } from '../mocks/resolver_tree'; import { SafeResolverEvent } from '../../../common/endpoint/types'; +import { mockTreeFetcherParameters } from '../mocks/tree_fetcher_parameters'; describe('resolver selectors', () => { const actions: ResolverAction[] = []; @@ -43,7 +44,7 @@ describe('resolver selectors', () => { secondAncestorID, }), // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); @@ -77,7 +78,7 @@ describe('resolver selectors', () => { payload: { result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }), // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 909a907626f30..8ea0bc9199cb6 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -84,14 +84,14 @@ export const layout: (state: ResolverState) => IsometricTaxiLayout = composeSele /** * If we need to fetch, this is the entity ID to fetch. */ -export const databaseDocumentIDToFetch = composeSelectors( +export const treeParametersToFetch = composeSelectors( dataStateSelector, - dataSelectors.databaseDocumentIDToFetch + dataSelectors.treeParametersToFetch ); -export const databaseDocumentIDToAbort = composeSelectors( +export const treeRequestParametersToAbort = composeSelectors( dataStateSelector, - dataSelectors.databaseDocumentIDToAbort + dataSelectors.treeRequestParametersToAbort ); export const resolverComponentInstanceID = composeSelectors( @@ -114,6 +114,18 @@ export const relatedEventsStats: ( dataSelectors.relatedEventsStats ); +/** + * This returns the "aggregate total" for related events, tallied as the sum + * of their individual `event.category`s. E.g. a [DNS, Network] would count as two + * towards the aggregate total. + */ +export const relatedEventAggregateTotalByEntityId: ( + state: ResolverState +) => (nodeID: string) => number = composeSelectors( + dataStateSelector, + dataSelectors.relatedEventAggregateTotalByEntityId +); + /** * Map of related events... by entity id */ @@ -195,12 +207,15 @@ function uiStateSelector(state: ResolverState) { /** * Whether or not the resolver is pending fetching data */ -export const isLoading = composeSelectors(dataStateSelector, dataSelectors.isLoading); +export const isTreeLoading = composeSelectors(dataStateSelector, dataSelectors.isTreeLoading); /** * Whether or not the resolver encountered an error while fetching data */ -export const hasError = composeSelectors(dataStateSelector, dataSelectors.hasError); +export const hadErrorLoadingTree = composeSelectors( + dataStateSelector, + dataSelectors.hadErrorLoadingTree +); /** * True if the children cursor is not null diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index a6520c8f0e06f..9d10d1c2b64a7 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -49,6 +49,7 @@ export class Simulator { dataAccessLayer, resolverComponentInstanceID, databaseDocumentID, + indices, history, }: { /** @@ -59,10 +60,14 @@ export class Simulator { * A string that uniquely identifies this Resolver instance among others mounted in the DOM. */ resolverComponentInstanceID: string; + /** + * Indices that the backend would use to find the document ID. + */ + indices: string[]; /** * a databaseDocumentID to pass to Resolver. Resolver will use this in requests to the mock data layer. */ - databaseDocumentID?: string; + databaseDocumentID: string; history?: HistoryPackageHistoryInterface; }) { // create the spy middleware (for debugging tests) @@ -99,6 +104,7 @@ export class Simulator { store={this.store} coreStart={coreStart} databaseDocumentID={databaseDocumentID} + indices={indices} /> ); } @@ -124,6 +130,20 @@ export class Simulator { this.wrapper.setProps({ resolverComponentInstanceID: value }); } + /** + * Change the indices (updates the React component props.) + */ + public set indices(value: string[]) { + this.wrapper.setProps({ indices: value }); + } + + /** + * Get the indices (updates the React component props.) + */ + public get indices(): string[] { + return this.wrapper.prop('indices'); + } + /** * Call this to console.log actions (and state). Use this to debug your tests. * State and actions aren't exposed otherwise because the tests using this simulator should diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx index 5d5a414761dbf..89218e9fca8ce 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx @@ -99,6 +99,7 @@ export const MockResolver = React.memo((props: MockResolverProps) => { ref={resolverRef} databaseDocumentID={props.databaseDocumentID} resolverComponentInstanceID={props.resolverComponentInstanceID} + indices={props.indices} /> diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index e8304bf838e2d..1e7e2a8eba8a8 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -194,53 +194,68 @@ export interface VisibleEntites { connectingEdgeLineSegments: EdgeLineSegment[]; } +export interface TreeFetcherParameters { + /** + * The `_id` for an ES document. Used to select a process that we'll show the graph for. + */ + databaseDocumentID: string; + + /** + * The indices that the backend will use to search for the document ID. + */ + indices: string[]; +} + /** * State for `data` reducer which handles receiving Resolver data from the back-end. */ export interface DataState { readonly relatedEvents: Map; readonly relatedEventsReady: Map; - /** - * The `_id` for an ES document. Used to select a process that we'll show the graph for. - */ - readonly databaseDocumentID?: string; - /** - * The id used for the pending request, if there is one. - */ - readonly pendingRequestDatabaseDocumentID?: string; + + readonly tree: { + /** + * The parameters passed from the resolver properties + */ + readonly currentParameters?: TreeFetcherParameters; + + /** + * The id used for the pending request, if there is one. + */ + readonly pendingRequestParameters?: TreeFetcherParameters; + /** + * The parameters and response from the last successful request. + */ + readonly lastResponse?: { + /** + * The id used in the request. + */ + readonly parameters: TreeFetcherParameters; + } & ( + | { + /** + * If a response with a success code was received, this is `true`. + */ + readonly successful: true; + /** + * The ResolverTree parsed from the response. + */ + readonly result: ResolverTree; + } + | { + /** + * If the request threw an exception or the response had a failure code, this will be false. + */ + readonly successful: false; + } + ); + }; /** * An ID that is used to differentiate this Resolver instance from others concurrently running on the same page. * Used to prevent collisions in things like query parameters. */ readonly resolverComponentInstanceID?: string; - - /** - * The parameters and response from the last successful request. - */ - readonly lastResponse?: { - /** - * The id used in the request. - */ - readonly databaseDocumentID: string; - } & ( - | { - /** - * If a response with a success code was received, this is `true`. - */ - readonly successful: true; - /** - * The ResolverTree parsed from the response. - */ - readonly result: ResolverTree; - } - | { - /** - * If the request threw an exception or the response had a failure code, this will be false. - */ - readonly successful: false; - } - ); } /** @@ -494,11 +509,6 @@ export interface DataAccessLayer { */ resolverTree: (entityID: string, signal: AbortSignal) => Promise; - /** - * Get an array of index patterns that contain events. - */ - indexPatterns: () => string[]; - /** * Get entities matching a document. */ @@ -524,13 +534,18 @@ export interface ResolverProps { * The `_id` value of an event in ES. * Used as the origin of the Resolver graph. */ - databaseDocumentID?: string; + databaseDocumentID: string; /** * An ID that is used to differentiate this Resolver instance from others concurrently running on the same page. * Used to prevent collisions in things like query parameters. */ resolverComponentInstanceID: string; + + /** + * Indices that the backend should use to find the originating document. + */ + indices: string[]; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 1e5ac093cac77..223ce728f4267 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { noAncestorsTwoChildenInIndexCalledAwesomeIndex } from '../data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index'; import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; import { Simulator } from '../test_utilities/simulator'; // Extend jest with a custom matcher @@ -19,6 +20,62 @@ let entityIDs: { origin: string; firstChild: string; secondChild: string }; // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances const resolverComponentInstanceID = 'resolverComponentInstanceID'; +describe("Resolver, when rendered with the `indices` prop set to `[]` and the `databaseDocumentID` prop set to `_id`, and when the document is found in an index called 'awesome_index'", () => { + beforeEach(async () => { + // create a mock data access layer + const { + metadata: dataAccessLayerMetadata, + dataAccessLayer, + } = noAncestorsTwoChildenInIndexCalledAwesomeIndex(); + + // save a reference to the entity IDs exposed by the mock data layer + entityIDs = dataAccessLayerMetadata.entityIDs; + + // save a reference to the `_id` supported by the mock data layer + databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; + + // create a resolver simulator, using the data access layer and an arbitrary component instance ID + simulator = new Simulator({ + databaseDocumentID, + dataAccessLayer, + resolverComponentInstanceID, + indices: [], + }); + }); + + it('should render no processes', async () => { + await expect( + simulator.map(() => ({ + processes: simulator.processNodeElements().length, + })) + ).toYieldEqualTo({ + processes: 0, + }); + }); + + describe("when rerendered with the `indices` prop set to `['awesome_index'`]", () => { + beforeEach(async () => { + simulator.indices = ['awesome_index']; + }); + // Combining assertions here for performance. Unfortunately, Enzyme + jsdom + React is slow. + it(`should have 3 nodes, with the entityID's 'origin', 'firstChild', and 'secondChild'. 'origin' should be selected when the simulator has the right indices`, async () => { + await expect( + simulator.map(() => ({ + selectedOriginCount: simulator.selectedProcessNode(entityIDs.origin).length, + unselectedFirstChildCount: simulator.unselectedProcessNode(entityIDs.firstChild).length, + unselectedSecondChildCount: simulator.unselectedProcessNode(entityIDs.secondChild).length, + nodePrimaryButtonCount: simulator.testSubject('resolver:node:primary-button').length, + })) + ).toYieldEqualTo({ + selectedOriginCount: 1, + unselectedFirstChildCount: 1, + unselectedSecondChildCount: 1, + nodePrimaryButtonCount: 3, + }); + }); + }); +}); + describe('Resolver, when analyzing a tree that has no ancestors and 2 children', () => { beforeEach(async () => { // create a mock data access layer @@ -31,7 +88,12 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; // create a resolver simulator, using the data access layer and an arbitrary component instance ID - simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID }); + simulator = new Simulator({ + databaseDocumentID, + dataAccessLayer, + resolverComponentInstanceID, + indices: [], + }); }); describe('when it has loaded', () => { @@ -159,7 +221,12 @@ describe('Resolver, when analyzing a tree that has two related events for the or databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; // create a resolver simulator, using the data access layer and an arbitrary component instance ID - simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID }); + simulator = new Simulator({ + databaseDocumentID, + dataAccessLayer, + resolverComponentInstanceID, + indices: [], + }); }); describe('when it has loaded', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx index 6497cc2971980..95fe68d95d702 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx @@ -34,6 +34,7 @@ describe('graph controls: when relsover is loaded with an origin node', () => { dataAccessLayer, databaseDocumentID, resolverComponentInstanceID, + indices: [], }); originEntityID = entityIDs.origin; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 1add907ae933d..7021e476e6439 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -49,6 +49,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an dataAccessLayer, resolverComponentInstanceID, history: memoryHistory, + indices: [], }); return simulatorInstance; } diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx index 98b737de8fa59..133dcd21e7f56 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx @@ -17,6 +17,7 @@ import { EventCountsForProcess } from './event_counts_for_process'; import { ProcessDetails } from './process_details'; import { ProcessListWithCounts } from './process_list_with_counts'; import { RelatedEventDetail } from './related_event_detail'; +import { ResolverState } from '../../types'; /** * The team decided to use this table to determine which breadcrumbs/view to display: @@ -102,6 +103,12 @@ const PanelContent = memo(function PanelContent() { ? relatedEventStats(idFromParams) : undefined; + const parentCount = useSelector((state: ResolverState) => { + if (idFromParams === '') { + return 0; + } + return selectors.relatedEventAggregateTotalByEntityId(state)(idFromParams); + }); /** * Determine which set of breadcrumbs to display based on the query parameters * for the table & breadcrumb nav. @@ -186,9 +193,6 @@ const PanelContent = memo(function PanelContent() { } if (panelToShow === 'relatedEventDetail') { - const parentCount: number = Object.values( - relatedStatsForIdFromParams?.events.byCategory || {} - ).reduce((sum, val) => sum + val, 0); return ( ; - }, [uiSelectedEvent, crumbEvent, crumbId, relatedStatsForIdFromParams, panelToShow]); + }, [uiSelectedEvent, crumbEvent, crumbId, relatedStatsForIdFromParams, panelToShow, parentCount]); return <>{panelInstance}; }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx index 01fa912caa866..4fcc557742643 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/process_details.tsx @@ -13,6 +13,7 @@ import { EuiText, EuiTextColor, EuiDescriptionList, + EuiLink, } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from 'react-intl'; @@ -58,6 +59,9 @@ export const ProcessDetails = memo(function ProcessDetails({ const isProcessTerminated = useSelector((state: ResolverState) => selectors.isProcessTerminated(state)(entityId) ); + const relatedEventTotal = useSelector((state: ResolverState) => { + return selectors.relatedEventAggregateTotalByEntityId(state)(entityId); + }); const processInfoEntry: EuiDescriptionListProps['listItems'] = useMemo(() => { const eventTime = event.eventTimestamp(processEvent); const dateTime = eventTime === undefined ? null : formatDate(eventTime); @@ -164,6 +168,12 @@ export const ProcessDetails = memo(function ProcessDetails({ return cubeAssetsForNode(isProcessTerminated, false); }, [processEvent, cubeAssetsForNode, isProcessTerminated]); + const handleEventsLinkClick = useMemo(() => { + return () => { + pushToQueryParams({ crumbId: entityId, crumbEvent: 'all' }); + }; + }, [entityId, pushToQueryParams]); + const titleID = useMemo(() => htmlIdGenerator('resolverTable')(), []); return ( <> @@ -185,6 +195,14 @@ export const ProcessDetails = memo(function ProcessDetails({ {descriptionText} + + + + { diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx index c357ee18acfeb..d8d8de640d786 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx @@ -26,6 +26,7 @@ describe('Resolver: data loading and resolution states', () => { dataAccessLayer, databaseDocumentID, resolverComponentInstanceID, + indices: [], }); }); @@ -56,6 +57,7 @@ describe('Resolver: data loading and resolution states', () => { dataAccessLayer, databaseDocumentID, resolverComponentInstanceID, + indices: [], }); }); @@ -85,6 +87,7 @@ describe('Resolver: data loading and resolution states', () => { dataAccessLayer, databaseDocumentID, resolverComponentInstanceID, + indices: [], }); }); @@ -114,6 +117,7 @@ describe('Resolver: data loading and resolution states', () => { dataAccessLayer, databaseDocumentID, resolverComponentInstanceID, + indices: [], }); }); @@ -145,6 +149,7 @@ describe('Resolver: data loading and resolution states', () => { dataAccessLayer, databaseDocumentID, resolverComponentInstanceID, + indices: [], }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index aa845e7283ebe..f4d471b384b35 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -31,7 +31,7 @@ export const ResolverWithoutProviders = React.memo( * Use `forwardRef` so that the `Simulator` used in testing can access the top level DOM element. */ React.forwardRef(function ( - { className, databaseDocumentID, resolverComponentInstanceID }: ResolverProps, + { className, databaseDocumentID, resolverComponentInstanceID, indices }: ResolverProps, refToForward ) { useResolverQueryParamCleaner(); @@ -39,7 +39,7 @@ export const ResolverWithoutProviders = React.memo( * This is responsible for dispatching actions that include any external data. * `databaseDocumentID` */ - useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID }); + useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID, indices }); const { timestamp } = useContext(SideEffectContext); @@ -69,8 +69,8 @@ export const ResolverWithoutProviders = React.memo( }, [cameraRef, refToForward] ); - const isLoading = useSelector(selectors.isLoading); - const hasError = useSelector(selectors.hasError); + const isLoading = useSelector(selectors.isTreeLoading); + const hasError = useSelector(selectors.hadErrorLoadingTree); const activeDescendantId = useSelector(selectors.ariaActiveDescendant); const { colorMap } = useResolverTheme(); diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index 630ee2f7ff7f0..495cd238d22fc 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -20,6 +20,7 @@ import { mock as mockResolverTree } from '../models/resolver_tree'; import { ResolverAction } from '../store/actions'; import { createStore } from 'redux'; import { resolverReducer } from '../store/reducer'; +import { mockTreeFetcherParameters } from '../mocks/tree_fetcher_parameters'; describe('useCamera on an unpainted element', () => { let element: HTMLElement; @@ -181,7 +182,7 @@ describe('useCamera on an unpainted element', () => { if (tree !== null) { const serverResponseAction: ResolverAction = { type: 'serverReturnedResolverData', - payload: { result: tree, databaseDocumentID: '' }, + payload: { result: tree, parameters: mockTreeFetcherParameters() }, }; act(() => { store.dispatch(serverResponseAction); diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts b/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts index eaba4438bb1fe..7f3cdcbec76a2 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts @@ -15,19 +15,21 @@ import { useResolverDispatch } from './use_resolver_dispatch'; export function useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID, + indices, }: { /** * The `_id` of an event in ES. Used to determine the origin of the Resolver graph. */ - databaseDocumentID?: string; + databaseDocumentID: string; resolverComponentInstanceID: string; + indices: string[]; }) { const dispatch = useResolverDispatch(); const locationSearch = useLocation().search; useLayoutEffect(() => { dispatch({ type: 'appReceivedNewExternalProperties', - payload: { databaseDocumentID, resolverComponentInstanceID, locationSearch }, + payload: { databaseDocumentID, resolverComponentInstanceID, locationSearch, indices }, }); - }, [dispatch, databaseDocumentID, resolverComponentInstanceID, locationSearch]); + }, [dispatch, databaseDocumentID, resolverComponentInstanceID, locationSearch, indices]); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index ededf70152968..dc9557da70f91 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -19,7 +19,7 @@ import styled from 'styled-components'; import { FULL_SCREEN } from '../timeline/body/column_headers/translations'; import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations'; -import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY, FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; import { useFullScreen } from '../../../common/containers/use_full_screen'; import { State } from '../../../common/store'; import { TimelineId, TimelineType } from '../../../../common/types/timeline'; @@ -33,6 +33,8 @@ import { Resolver } from '../../../resolver/view'; import { useAllCasesModal } from '../../../cases/components/use_all_cases_modal'; import * as i18n from './translations'; +import { useUiSetting$ } from '../../../common/lib/kibana'; +import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; const OverlayContainer = styled.div` height: 100%; @@ -137,6 +139,16 @@ const GraphOverlayComponent = ({ globalFullScreen, ]); + const { signalIndexName } = useSignalIndex(); + const [siemDefaultIndices] = useUiSetting$(DEFAULT_INDEX_KEY); + const indices: string[] | null = useMemo(() => { + if (signalIndexName === null) { + return null; + } else { + return [...siemDefaultIndices, signalIndexName]; + } + }, [signalIndexName, siemDefaultIndices]); + return ( @@ -178,10 +190,13 @@ const GraphOverlayComponent = ({ - + {graphEventId !== undefined && indices !== null && ( + + )} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index c91fc473708e2..9691327f2c988 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -11,8 +11,9 @@ import VisibilitySensor from 'react-visibility-sensor'; import { TimelineId } from '../../../../../../common/types/timeline'; import { BrowserFields, DocValueFields } from '../../../../../common/containers/source'; -import { TimelineDetailsQuery } from '../../../../containers/details'; -import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../../graphql/types'; +import { useTimelineDetails } from '../../../../containers/details'; +import { TimelineItem, TimelineNonEcsData } from '../../../../../graphql/types'; +import { DetailItem } from '../../../../../../common/search_strategy/timeline'; import { Note } from '../../../../../common/lib/note'; import { ColumnHeaderOptions, TimelineModel } from '../../../../../timelines/store/timeline/model'; import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; @@ -137,6 +138,12 @@ const StatefulEventComponent: React.FC = ({ (state) => state.timeline.timelineById[TimelineId.active] ); const divElement = useRef(null); + const [loading, detailsData] = useTimelineDetails({ + docValueFields, + indexName: event._index!, + eventId: event._id, + executeQuery: !!expanded[event._id], + }); const onToggleShowNotes = useCallback(() => { const eventId = event._id; @@ -174,94 +181,84 @@ const StatefulEventComponent: React.FC = ({ {({ isVisible }) => { if (isVisible || disableSensorVisibility) { return ( - - {({ detailsData, loading }) => ( - + + + - + - - - - - - {getRowRenderer(event.ecs, rowRenderers).renderRow({ - browserFields, - data: event.ecs, - timelineId, - })} + {getRowRenderer(event.ecs, rowRenderers).renderRow({ + browserFields, + data: event.ecs, + timelineId, + })} - - - - - - )} - + + + + + ); } else { // Height place holder for visibility detection as well as re-rendering sections. diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx index 269cd14b5973c..49f17db242f75 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../../../common/containers/source'; import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; -import { DetailItem } from '../../../../graphql/types'; +import { DetailItem } from '../../../../../common/search_strategy/timeline'; import { StatefulEventDetails } from '../../../../common/components/event_details/stateful_event_details'; import { LazyAccordion } from '../../lazy_accordion'; import { OnUpdateColumns } from '../events'; @@ -51,27 +51,43 @@ export const ExpandableEvent = React.memo( toggleColumn, onEventToggled, onUpdateColumns, - }) => ( - - ( - - )} - forceExpand={forceExpand} - paddingSize="none" - /> - - ) + }) => { + const handleRenderExpandedContent = useCallback( + () => ( + + ), + [ + browserFields, + columnHeaders, + event, + id, + onEventToggled, + onUpdateColumns, + timelineId, + toggleColumn, + ] + ); + + return ( + + + + ); + } ); ExpandableEvent.displayName = 'ExpandableEvent'; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx index a07420dead29b..35f8c7ae90e6e 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx @@ -4,71 +4,129 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; +import { noop } from 'lodash/fp'; import memoizeOne from 'memoize-one'; -import React from 'react'; -import { Query } from 'react-apollo'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; +import { inputsModel } from '../../../common/store'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { DetailItem, GetTimelineDetailsQuery } from '../../../graphql/types'; -import { useUiSetting } from '../../../common/lib/kibana'; - -import { timelineDetailsQuery } from './index.gql_query'; -import { DocValueFields } from '../../../common/containers/source'; - +import { useKibana } from '../../../common/lib/kibana'; +import { + DocValueFields, + DetailItem, + TimelineQueries, + TimelineDetailsRequestOptions, + TimelineDetailsStrategyResponse, +} from '../../../../common/search_strategy'; export interface EventsArgs { detailsData: DetailItem[] | null; loading: boolean; } export interface TimelineDetailsProps { - children?: (args: EventsArgs) => React.ReactElement; docValueFields: DocValueFields[]; indexName: string; eventId: string; executeQuery: boolean; - sourceId: string; } const getDetailsEvent = memoizeOne( (variables: string, detail: DetailItem[]): DetailItem[] => detail ); -const TimelineDetailsQueryComponent: React.FC = ({ - children, +export const useTimelineDetails = ({ docValueFields, indexName, eventId, executeQuery, - sourceId, -}) => { - const variables: GetTimelineDetailsQuery.Variables = { +}: TimelineDetailsProps): [boolean, EventsArgs['detailsData']] => { + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + const [timelineDetailsRequest, setTimelineDetailsRequest] = useState< + TimelineDetailsRequestOptions + >({ + defaultIndex, docValueFields, - sourceId, + executeQuery, indexName, eventId, - defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), - }; - return executeQuery ? ( - - query={timelineDetailsQuery} - fetchPolicy="network-only" - notifyOnNetworkStatusChange - variables={variables} - > - {({ data, loading, refetch }) => - children!({ - loading, - detailsData: getDetailsEvent( - JSON.stringify(variables), - getOr([], 'source.TimelineDetails.data', data) - ), - }) - } - - ) : ( - children!({ loading: false, detailsData: null }) + factoryQueryType: TimelineQueries.details, + }); + + const [timelineDetailsResponse, setTimelineDetailsResponse] = useState( + null ); -}; -export const TimelineDetailsQuery = React.memo(TimelineDetailsQueryComponent); + const timelineDetailsSearch = useCallback( + (request: TimelineDetailsRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionTimelineSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setTimelineDetailsResponse( + getDetailsEvent(JSON.stringify(timelineDetailsRequest), response.data || []) + ); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning('An error has occurred'); + searchSubscription$.unsubscribe(); + } + }, + error: () => { + notifications.toasts.addDanger('Failed to run search'); + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts, timelineDetailsRequest] + ); + + useEffect(() => { + setTimelineDetailsRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + docValueFields, + indexName, + eventId, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [defaultIndex, docValueFields, eventId, indexName]); + + useEffect(() => { + if (executeQuery) timelineDetailsSearch(timelineDetailsRequest); + }, [executeQuery, timelineDetailsRequest, timelineDetailsSearch]); + + return [loading, timelineDetailsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index d2b66207d8602..62069484dd8bd 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -22,6 +22,7 @@ import { import { SecurityPluginSetup } from '../../security/public'; import { AppFrontendLibs } from './common/lib/lib'; import { ResolverPluginSetup } from './resolver/types'; +import { Inspect } from '../common/search_strategy'; export interface SetupPlugins { home?: HomePublicPluginSetup; @@ -56,3 +57,5 @@ export interface PluginStart {} export interface AppObservableLibs extends AppFrontendLibs { kibana: CoreStart; } + +export type InspectResponse = Inspect & { response: string[] }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts index bcac559d61f79..510bb6c545558 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts @@ -18,11 +18,6 @@ export function handleEntities(): RequestHandler => { + const logger = endpointAppContext.logFactory.get('trusted_apps'); + + return async (context, req, res) => { + const exceptionsListService = endpointAppContext.service.getExceptionsList(); + + try { + const { id } = req.params; + const response = await exceptionsListService.deleteExceptionListItem({ + id, + itemId: undefined, + namespaceType: 'agnostic', + }); + + if (response === null) { + return res.notFound({ body: `trusted app id [${id}] not found` }); + } + + return res.ok(); + } catch (error) { + logger.error(error); + return res.internalError({ body: error }); + } + }; +}; export const getTrustedAppsListRouteHandler = ( endpointAppContext: EndpointAppContext diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts index 1302b10533ccf..5f502555ee153 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts @@ -6,20 +6,36 @@ import { IRouter } from 'kibana/server'; import { + DeleteTrustedAppsRequestSchema, GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema, } from '../../../../common/endpoint/schema/trusted_apps'; import { TRUSTED_APPS_CREATE_API, + TRUSTED_APPS_DELETE_API, TRUSTED_APPS_LIST_API, } from '../../../../common/endpoint/constants'; -import { getTrustedAppsCreateRouteHandler, getTrustedAppsListRouteHandler } from './handlers'; +import { + getTrustedAppsCreateRouteHandler, + getTrustedAppsDeleteRouteHandler, + getTrustedAppsListRouteHandler, +} from './handlers'; import { EndpointAppContext } from '../../types'; export const registerTrustedAppsRoutes = ( router: IRouter, endpointAppContext: EndpointAppContext ) => { + // DELETE one + router.delete( + { + path: TRUSTED_APPS_DELETE_API, + validate: DeleteTrustedAppsRequestSchema, + options: { authRequired: true }, + }, + getTrustedAppsDeleteRouteHandler(endpointAppContext) + ); + // GET list router.get( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts index 488c8390411b0..2325036ef40ae 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts @@ -9,11 +9,12 @@ import { createMockEndpointAppContext, createMockEndpointAppContextServiceStartContract, } from '../../mocks'; -import { IRouter, RequestHandler } from 'kibana/server'; +import { IRouter, KibanaRequest, RequestHandler } from 'kibana/server'; import { httpServerMock, httpServiceMock } from '../../../../../../../src/core/server/mocks'; import { registerTrustedAppsRoutes } from './index'; import { TRUSTED_APPS_CREATE_API, + TRUSTED_APPS_DELETE_API, TRUSTED_APPS_LIST_API, } from '../../../../common/endpoint/constants'; import { @@ -26,6 +27,7 @@ import { EndpointAppContext } from '../../types'; import { ExceptionListClient } from '../../../../../lists/server'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response'; +import { DeleteTrustedAppsRequestParams } from './types'; describe('when invoking endpoint trusted apps route handlers', () => { let routerMock: jest.Mocked; @@ -220,4 +222,45 @@ describe('when invoking endpoint trusted apps route handlers', () => { expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); }); }); + + describe('when deleting a trusted app', () => { + let routeHandler: RequestHandler; + let request: KibanaRequest; + + beforeEach(() => { + [, routeHandler] = routerMock.delete.mock.calls.find(([{ path }]) => + path.startsWith(TRUSTED_APPS_DELETE_API) + )!; + + request = httpServerMock.createKibanaRequest({ + path: TRUSTED_APPS_DELETE_API.replace('{id}', '123'), + method: 'delete', + }); + }); + + it('should return 200 on successful delete', async () => { + await routeHandler(context, request, response); + expect(exceptionsListClient.deleteExceptionListItem).toHaveBeenCalledWith({ + id: request.params.id, + itemId: undefined, + namespaceType: 'agnostic', + }); + expect(response.ok).toHaveBeenCalled(); + }); + + it('should return 404 if item does not exist', async () => { + exceptionsListClient.deleteExceptionListItem.mockResolvedValueOnce(null); + await routeHandler(context, request, response); + expect(response.notFound).toHaveBeenCalled(); + }); + + it('should log unexpected error if one occurs', async () => { + exceptionsListClient.deleteExceptionListItem.mockImplementation(() => { + throw new Error('expected error for delete'); + }); + await routeHandler(context, request, response); + expect(response.internalError).toHaveBeenCalled(); + expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts new file mode 100644 index 0000000000000..13c8bcfc20793 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts @@ -0,0 +1,10 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { DeleteTrustedAppsRequestSchema } from '../../../../common/endpoint/schema/trusted_apps'; + +export type DeleteTrustedAppsRequestParams = TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index 0a899562d61c2..802b9472a4487 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -14,7 +14,7 @@ import { RuleAlertAttributes } from '../signals/types'; import { siemRuleActionGroups } from '../signals/siem_rule_action_groups'; import { scheduleNotificationActions } from './schedule_notification_actions'; import { getNotificationResultsLink } from './utils'; -import { parseScheduleDates } from '../../../../common/detection_engine/utils'; +import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; export const rulesNotificationAlertType = ({ logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 18eea7c45585f..8a7215e5a5bad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -20,7 +20,7 @@ import { importRulesSchema as importRulesResponseSchema, } from '../../../../../common/detection_engine/schemas/response/import_rules_schema'; import { IRouter } from '../../../../../../../../src/core/server'; -import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; +import { createPromiseFromStreams } from '../../../../../../../../src/core/server/utils/'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { ConfigType } from '../../../../config'; import { SetupPlugins } from '../../../../plugin'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index 3122db1919c3c..11f74c264ae0c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -22,7 +22,7 @@ import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { RuleTypeParams } from '../../types'; import { BulkError, ImportSuccessError } from '../utils'; import { getOutputRuleAlertForRest } from '../__mocks__/utils'; -import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; +import { createPromiseFromStreams } from '../../../../../../../../src/core/server/utils'; import { PartialAlert } from '../../../../../../alerts/server'; import { SanitizedAlert } from '../../../../../../alerts/server/types'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts index f2061ce1d36de..60071bc2cef41 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -5,7 +5,7 @@ */ import { Readable } from 'stream'; import { createRulesStreamFromNdJson } from './create_rules_stream_from_ndjson'; -import { createPromiseFromStreams } from 'src/legacy/utils/streams'; +import { createPromiseFromStreams } from 'src/core/server/utils'; import { BadRequestError } from '../errors/bad_request_error'; import { ImportRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/import_rules_schema'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts index d7723232ca921..cd574a8d95615 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.ts @@ -19,7 +19,7 @@ import { createSplitStream, createMapStream, createConcatStream, -} from '../../../../../../../src/legacy/utils/streams'; +} from '../../../../../../../src/core/server/utils'; import { BadRequestError } from '../errors/bad_request_error'; import { parseNdjsonStrings, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts index 18a64a12431b8..bd9bf50688b58 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts @@ -6,13 +6,12 @@ import dateMath from '@elastic/datemath'; -import { ILegacyScopedClusterClient, KibanaRequest } from '../../../../../../../src/core/server'; +import { KibanaRequest } from '../../../../../../../src/core/server'; import { MlPluginSetup } from '../../../../../ml/server'; import { getAnomalies } from '../../machine_learning'; export const findMlSignals = async ({ ml, - clusterClient, request, jobId, anomalyThreshold, @@ -20,14 +19,13 @@ export const findMlSignals = async ({ to, }: { ml: MlPluginSetup; - clusterClient: ILegacyScopedClusterClient; request: KibanaRequest; jobId: string; anomalyThreshold: number; from: string; to: string; }) => { - const { mlAnomalySearch } = ml.mlSystemProvider(clusterClient, request); + const { mlAnomalySearch } = ml.mlSystemProvider(request); const params = { jobIds: [jobId], threshold: anomalyThreshold, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index a7213c30eb3fb..b8311182f3ca8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -17,7 +17,7 @@ import { getExceptions, sortExceptionItems, } from './utils'; -import { parseScheduleDates } from '../../../../common/detection_engine/utils'; +import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { RuleExecutorOptions } from './types'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; @@ -38,6 +38,7 @@ jest.mock('../notifications/schedule_notification_actions'); jest.mock('./find_ml_signals'); jest.mock('./bulk_create_ml_signals'); jest.mock('./../../../../common/detection_engine/utils'); +jest.mock('../../../../common/detection_engine/parse_schedule_dates'); const getPayload = (ruleAlert: RuleAlertType, services: AlertServicesMock) => ({ alertId: ruleAlert.id, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index c5124edcaf187..da17d4a1f123a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -14,7 +14,7 @@ import { SERVER_APP_ID, } from '../../../../common/constants'; import { isJobStarted, isMlRule } from '../../../../common/machine_learning/helpers'; -import { parseScheduleDates } from '../../../../common/detection_engine/utils'; +import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { SetupPlugins } from '../../../plugin'; import { getInputIndex } from './get_input_output_index'; import { @@ -185,12 +185,12 @@ export const signalRulesAlertType = ({ ); } - const scopedClusterClient = services.getLegacyScopedClusterClient(ml.mlClient); // Using fake KibanaRequest as it is needed to satisfy the ML Services API, but can be empty as it is // currently unused by the jobsSummary function. - const summaryJobs = await ( - await ml.jobServiceProvider(scopedClusterClient, ({} as unknown) as KibanaRequest) - ).jobsSummary([machineLearningJobId]); + const fakeRequest = {} as KibanaRequest; + const summaryJobs = await ml + .jobServiceProvider(fakeRequest) + .jobsSummary([machineLearningJobId]); const jobSummary = summaryJobs.find((job) => job.id === machineLearningJobId); if (jobSummary == null || !isJobStarted(jobSummary.jobState, jobSummary.datafeedState)) { @@ -207,7 +207,6 @@ export const signalRulesAlertType = ({ const anomalyResults = await findMlSignals({ ml, - clusterClient: scopedClusterClient, // Using fake KibanaRequest as it is needed to satisfy the ML Services API, but can be empty as it is // currently unused by the mlAnomalySearch function. request: ({} as unknown) as KibanaRequest, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index a2e2fec3309c3..d053a8e1089ad 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -13,7 +13,7 @@ import { buildRuleMessageFactory } from './rule_messages'; import { ExceptionListClient } from '../../../../../lists/server'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { parseScheduleDates } from '../../../../common/detection_engine/utils'; +import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { generateId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 92cc9be69839f..dc09c6d5386fc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -14,7 +14,8 @@ import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; import { BulkResponse, BulkResponseErrorAggregation, isValidUnit } from './types'; import { BuildRuleMessage } from './rule_messages'; -import { hasLargeValueList, parseScheduleDates } from '../../../../common/detection_engine/utils'; +import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; +import { hasLargeValueList } from '../../../../common/detection_engine/utils'; import { MAX_EXCEPTION_LIST_SIZE } from '../../../../../lists/common/constants'; interface SortExceptionsReturn { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts index 4b4f5147c9a42..cbe756064b72b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts @@ -36,10 +36,10 @@ import { RuleNameOverrideOrUndefined, SeverityMappingOrUndefined, TimestampOverrideOrUndefined, + Type, } from '../../../common/detection_engine/schemas/common/schemas'; import { LegacyCallAPIOptions } from '../../../../../../src/core/server'; import { Filter } from '../../../../../../src/plugins/data/server'; -import { RuleType } from '../../../common/detection_engine/types'; import { ListArrayOrUndefined } from '../../../common/detection_engine/schemas/types'; export type PartialFilter = Partial; @@ -75,7 +75,7 @@ export interface RuleTypeParams { threshold: ThresholdOrUndefined; timestampOverride: TimestampOverrideOrUndefined; to: To; - type: RuleType; + type: Type; references: References; version: Version; exceptionsList: ListArrayOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts index 98de9536b1baa..ff565c05848f9 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/authz.ts @@ -13,12 +13,12 @@ import { SetupPlugins } from '../../plugin'; import { MINIMUM_ML_LICENSE } from '../../../common/constants'; import { hasMlAdminPermissions } from '../../../common/machine_learning/has_ml_admin_permissions'; import { isMlRule } from '../../../common/machine_learning/helpers'; -import { RuleType } from '../../../common/detection_engine/types'; import { Validation } from './validation'; import { cache } from './cache'; +import { Type } from '../../../common/detection_engine/schemas/common/schemas'; export interface MlAuthz { - validateRuleType: (type: RuleType) => Promise; + validateRuleType: (type: Type) => Promise; } /** @@ -40,7 +40,7 @@ export const buildMlAuthz = ({ request: KibanaRequest; }): MlAuthz => { const cachedValidate = cache(() => validateMlAuthz({ license, ml, request })); - const validateRuleType = async (type: RuleType): Promise => { + const validateRuleType = async (type: Type): Promise => { if (!isMlRule(type)) { return { valid: true, message: undefined }; } else { @@ -114,7 +114,6 @@ export const isMlAdmin = async ({ request: KibanaRequest; ml: MlPluginSetup; }): Promise => { - const scopedMlClient = ml.mlClient.asScoped(request); - const mlCapabilities = await ml.mlSystemProvider(scopedMlClient, request).mlCapabilities(); + const mlCapabilities = await ml.mlSystemProvider(request).mlCapabilities(); return hasMlAdminPermissions(mlCapabilities); }; diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts index ad2f1e5a8285c..c17d9ab1bb46e 100644 --- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts +++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse, SearchParams } from 'elasticsearch'; +import { SearchResponse } from 'elasticsearch'; +import { RequestParams } from '@elastic/elasticsearch'; import { AnomalyRecordDoc as Anomaly } from '../../../../ml/server'; export { Anomaly }; export type AnomalyResults = SearchResponse; -type MlAnomalySearch = (searchParams: SearchParams) => Promise>; +type MlAnomalySearch = (searchParams: RequestParams.Search) => Promise>; export interface AnomaliesSearchParams { jobIds: string[]; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/create_timelines_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/lib/timeline/create_timelines_stream_from_ndjson.ts index 6b4017b5e4d5c..2827cd373d5e7 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/create_timelines_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/create_timelines_stream_from_ndjson.ts @@ -13,7 +13,7 @@ import { createConcatStream, createSplitStream, createMapStream, -} from '../../../../../../src/legacy/utils'; +} from '../../../../../../src/core/server/utils'; import { parseNdjsonStrings, filterExportedCounts, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts index c5d69398b7f0c..026ec1fa847f9 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import path, { join, resolve } from 'path'; import * as rt from 'io-ts'; -import stream from 'stream'; import { TIMELINE_DRAFT_URL, @@ -20,8 +21,8 @@ import { requestMock } from '../../../detection_engine/routes/__mocks__'; import { updateTimelineSchema } from '../schemas/update_timelines_schema'; import { createTimelineSchema } from '../schemas/create_timelines_schema'; import { GetTimelineByIdSchemaQuery } from '../schemas/get_timeline_by_id_schema'; +import { getReadables } from '../utils/common'; -const readable = new stream.Readable(); export const getExportTimelinesRequest = () => requestMock.create({ method: 'get', @@ -34,15 +35,20 @@ export const getExportTimelinesRequest = () => }, }); -export const getImportTimelinesRequest = (filename?: string) => - requestMock.create({ +export const getImportTimelinesRequest = async (fileName?: string) => { + const dir = resolve(join(__dirname, '../../../detection_engine/rules/prepackaged_timelines')); + const file = fileName ?? 'index.ndjson'; + const dataPath = path.join(dir, file); + const readable = await getReadables(dataPath); + return requestMock.create({ method: 'post', path: TIMELINE_IMPORT_URL, query: { overwrite: false }, body: { - file: { ...readable, hapi: { filename: filename ?? 'filename.ndjson' } }, + file: { ...readable, hapi: { filename: file } }, }, }); +}; export const inputTimeline: SavedTimeline = { columns: [ diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts index 8cabd84a965b7..67fc3167a4a29 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts @@ -11,7 +11,7 @@ import { transformError, buildSiemResponse } from '../../detection_engine/routes import { TIMELINE_DRAFT_URL } from '../../../../common/constants'; import { buildFrameworkRequest } from './utils/common'; import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { getDraftTimeline, resetTimeline, getTimeline, persistTimeline } from '../saved_object'; import { draftTimelineDefaults } from '../default_timeline'; import { cleanDraftTimelineSchema } from './schemas/clean_draft_timelines_schema'; @@ -26,7 +26,7 @@ export const cleanDraftTimelinesRoute = ( { path: TIMELINE_DRAFT_URL, validate: { - body: buildRouteValidation(cleanDraftTimelineSchema), + body: buildRouteValidationWithExcess(cleanDraftTimelineSchema), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts index 7abcb390d0221..77cd49406baa1 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts @@ -9,7 +9,7 @@ import { TIMELINE_URL } from '../../../../common/constants'; import { ConfigType } from '../../..'; import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; @@ -31,7 +31,7 @@ export const createTimelinesRoute = ( { path: TIMELINE_URL, validate: { - body: buildRouteValidation(createTimelineSchema), + body: buildRouteValidationWithExcess(createTimelineSchema), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts index 5a976ee7521af..5d7cb1c8d3f6c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts @@ -96,7 +96,7 @@ describe('export timelines', () => { const result = server.validate(request); expect(result.badRequest.mock.calls[0][0]).toEqual( - 'Invalid value "undefined" supplied to "file_name"' + 'Invalid value {"id":"someId"}, excess properties: ["id"]' ); }); @@ -110,7 +110,7 @@ describe('export timelines', () => { const result = server.validate(request); expect(result.badRequest.mock.calls[0][0]).toEqual( - 'Invalid value "someId" supplied to "ids",Invalid value "{"ids":"someId"}" supplied to "(Partial<{ ids: (Array | null) }> | null)"' + 'Invalid value "someId" supplied to "ids"' ); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts index 89e38753ac926..38ee51fb7aa0c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.ts @@ -14,7 +14,7 @@ import { exportTimelinesQuerySchema, exportTimelinesRequestBodySchema, } from './schemas/export_timelines_schema'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { buildFrameworkRequest } from './utils/common'; import { SetupPlugins } from '../../../plugin'; @@ -27,8 +27,8 @@ export const exportTimelinesRoute = ( { path: TIMELINE_EXPORT_URL, validate: { - query: buildRouteValidation(exportTimelinesQuerySchema), - body: buildRouteValidation(exportTimelinesRequestBodySchema), + query: buildRouteValidationWithExcess(exportTimelinesQuerySchema), + body: buildRouteValidationWithExcess(exportTimelinesRequestBodySchema), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.ts index 4db434ec816aa..43129f0e15f0e 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_draft_timelines_route.ts @@ -10,7 +10,7 @@ import { transformError, buildSiemResponse } from '../../detection_engine/routes import { TIMELINE_DRAFT_URL } from '../../../../common/constants'; import { buildFrameworkRequest } from './utils/common'; import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { getDraftTimeline, persistTimeline } from '../saved_object'; import { draftTimelineDefaults } from '../default_timeline'; import { getDraftTimelineSchema } from './schemas/get_draft_timelines_schema'; @@ -24,7 +24,7 @@ export const getDraftTimelinesRoute = ( { path: TIMELINE_DRAFT_URL, validate: { - query: buildRouteValidation(getDraftTimelineSchema), + query: buildRouteValidationWithExcess(getDraftTimelineSchema), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts index f36adb648cc03..e46a644d6820e 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/get_timeline_route.ts @@ -10,7 +10,7 @@ import { TIMELINE_URL } from '../../../../common/constants'; import { ConfigType } from '../../..'; import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { buildSiemResponse, transformError } from '../../detection_engine/routes/utils'; @@ -28,7 +28,7 @@ export const getTimelineRoute = ( router.get( { path: `${TIMELINE_URL}`, - validate: { query: buildRouteValidation(getTimelineByIdSchemaQuery) }, + validate: { query: buildRouteValidationWithExcess(getTimelineByIdSchemaQuery) }, options: { tags: ['access:securitySolution'], }, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts index ff76045db90cb..13cc71840ec96 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts @@ -79,7 +79,7 @@ describe('import timelines', () => { }; }); - jest.doMock('../../../../../../../src/legacy/utils', () => { + jest.doMock('../../../../../../../src/core/server/utils', () => { return { createPromiseFromStreams: jest.fn().mockReturnValue(mockParsedObjects), }; @@ -155,31 +155,31 @@ describe('import timelines', () => { }); test('should use given timelineId to check if the timeline savedObject already exist', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockGetTimeline.mock.calls[0][1]).toEqual(mockUniqueParsedObjects[0].savedObjectId); }); test('should Create a new timeline savedObject', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline).toHaveBeenCalled(); }); test('should Create a new timeline savedObject without timelineId', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][1]).toBeNull(); }); test('should Create a new timeline savedObject without timeline version', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][2]).toBeNull(); }); test('should Create a new timeline savedObject with given timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][3]).toEqual({ ...mockParsedTimelineObject, @@ -199,7 +199,7 @@ describe('import timelines', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); const response = await server.inject(mockRequest, context); expect(response.body).toEqual({ success: false, @@ -219,19 +219,19 @@ describe('import timelines', () => { }); test('should Create new pinned events', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline).toHaveBeenCalled(); }); test('should Create a new pinned event without pinnedEventSavedObjectId', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline.mock.calls[0][1]).toBeNull(); }); test('should Create a new pinned event with pinnedEventId', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline.mock.calls[0][2]).toEqual( mockUniqueParsedObjects[0].pinnedEventIds[0] @@ -239,7 +239,7 @@ describe('import timelines', () => { }); test('should Create a new pinned event with new timelineSavedObjectId', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline.mock.calls[0][3]).toEqual( mockCreatedTimeline.savedObjectId @@ -247,7 +247,7 @@ describe('import timelines', () => { }); test('should Check if note exists', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockGetNote.mock.calls[0][1]).toEqual( mockUniqueParsedObjects[0].globalNotes[0].noteId @@ -255,31 +255,31 @@ describe('import timelines', () => { }); test('should Create notes', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote).toHaveBeenCalled(); }); test('should provide no noteSavedObjectId when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][1]).toBeNull(); }); test('should provide new timeline version when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][1]).toBeNull(); }); test('should provide note content when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTimeline.version); }); test('should provide new notes with original author info when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][3]).toEqual({ eventId: undefined, @@ -314,7 +314,7 @@ describe('import timelines', () => { mockGetNote.mockReset(); mockGetNote.mockRejectedValue(new Error()); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][3]).toEqual({ created: mockUniqueParsedObjects[0].globalNotes[0].created, @@ -346,7 +346,8 @@ describe('import timelines', () => { }); test('returns 200 when import timeline successfully', async () => { - const response = await server.inject(getImportTimelinesRequest(), context); + const mockRequest = await getImportTimelinesRequest(); + const response = await server.inject(mockRequest, context); expect(response.status).toEqual(200); }); }); @@ -379,7 +380,8 @@ describe('import timelines', () => { }); test('returns error message', async () => { - const response = await server.inject(getImportTimelinesRequest(), context); + const mockRequest = await getImportTimelinesRequest(); + const response = await server.inject(mockRequest, context); expect(response.body).toEqual({ success: false, success_count: 0, @@ -407,7 +409,7 @@ describe('import timelines', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); const response = await server.inject(mockRequest, context); expect(response.body).toEqual({ success: false, @@ -436,7 +438,7 @@ describe('import timelines', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); const response = await server.inject(mockRequest, context); expect(response.body).toEqual({ success: false, @@ -494,7 +496,7 @@ describe('import timelines', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "undefined" supplied to "file"' + 'Invalid value {"id":"someId"}, excess properties: ["id"]' ); }); }); @@ -543,7 +545,7 @@ describe('import timeline templates', () => { }; }); - jest.doMock('../../../../../../../src/legacy/utils', () => { + jest.doMock('../../../../../../../src/core/server/utils', () => { return { createPromiseFromStreams: jest.fn().mockReturnValue(mockParsedTemplateTimelineObjects), }; @@ -592,7 +594,7 @@ describe('import timeline templates', () => { }); test('should use given timelineId to check if the timeline savedObject already exist', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockGetTimeline.mock.calls[0][1]).toEqual( mockUniqueParsedTemplateTimelineObjects[0].savedObjectId @@ -600,7 +602,7 @@ describe('import timeline templates', () => { }); test('should use given templateTimelineId to check if the timeline savedObject already exist', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual( mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId @@ -608,25 +610,25 @@ describe('import timeline templates', () => { }); test('should Create a new timeline savedObject', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline).toHaveBeenCalled(); }); test('should Create a new timeline savedObject without timelineId', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][1]).toBeNull(); }); test('should Create a new timeline savedObject without timeline version', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][2]).toBeNull(); }); test('should Create a new timeline savedObject witn given timeline and skip the omitted fields', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][3]).toEqual({ ...mockParsedTemplateTimelineObject, @@ -635,25 +637,25 @@ describe('import timeline templates', () => { }); test('should NOT Create new pinned events', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline).not.toHaveBeenCalled(); }); test('should provide no noteSavedObjectId when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][1]).toBeNull(); }); test('should provide new timeline version when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTemplateTimeline.version); }); test('should exclude event notes when creating notes', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][3]).toEqual({ eventId: undefined, @@ -667,7 +669,8 @@ describe('import timeline templates', () => { }); test('returns 200 when import timeline successfully', async () => { - const response = await server.inject(getImportTimelinesRequest(), context); + const mockRequest = await getImportTimelinesRequest(); + const response = await server.inject(mockRequest, context); expect(response.status).toEqual(200); }); @@ -681,7 +684,7 @@ describe('import timeline templates', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][3].templateTimelineId).toEqual( mockNewTemplateTimelineId @@ -699,7 +702,7 @@ describe('import timeline templates', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); const result = await server.inject(mockRequest, context); expect(result.body).toEqual({ errors: [], @@ -743,7 +746,7 @@ describe('import timeline templates', () => { }); test('should use given timelineId to check if the timeline savedObject already exist', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockGetTimeline.mock.calls[0][1]).toEqual( mockUniqueParsedTemplateTimelineObjects[0].savedObjectId @@ -751,7 +754,7 @@ describe('import timeline templates', () => { }); test('should use given templateTimelineId to check if the timeline savedObject already exist', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual( mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId @@ -759,13 +762,13 @@ describe('import timeline templates', () => { }); test('should UPDATE timeline savedObject', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline).toHaveBeenCalled(); }); test('should UPDATE timeline savedObject with timelineId', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][1]).toEqual( mockUniqueParsedTemplateTimelineObjects[0].savedObjectId @@ -773,7 +776,7 @@ describe('import timeline templates', () => { }); test('should UPDATE timeline savedObject without timeline version', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][2]).toEqual( mockUniqueParsedTemplateTimelineObjects[0].version @@ -781,31 +784,31 @@ describe('import timeline templates', () => { }); test('should UPDATE a new timeline savedObject witn given timeline and skip the omitted fields', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistTimeline.mock.calls[0][3]).toEqual(mockParsedTemplateTimelineObject); }); test('should NOT Create new pinned events', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistPinnedEventOnTimeline).not.toHaveBeenCalled(); }); test('should provide noteSavedObjectId when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][1]).toBeNull(); }); test('should provide new timeline version when Creating notes for a timeline', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTemplateTimeline.version); }); test('should exclude event notes when creating notes', async () => { - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); await server.inject(mockRequest, context); expect(mockPersistNote.mock.calls[0][3]).toEqual({ eventId: undefined, @@ -819,7 +822,8 @@ describe('import timeline templates', () => { }); test('returns 200 when import timeline successfully', async () => { - const response = await server.inject(getImportTimelinesRequest(), context); + const mockRequest = await getImportTimelinesRequest(); + const response = await server.inject(mockRequest, context); expect(response.status).toEqual(200); }); @@ -833,7 +837,7 @@ describe('import timeline templates', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); const response = await server.inject(mockRequest, context); expect(response.body).toEqual({ success: false, @@ -862,7 +866,7 @@ describe('import timeline templates', () => { }, ], ]); - const mockRequest = getImportTimelinesRequest(); + const mockRequest = await getImportTimelinesRequest(); const response = await server.inject(mockRequest, context); expect(response.body).toEqual({ success: false, @@ -920,7 +924,7 @@ describe('import timeline templates', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'Invalid value "undefined" supplied to "file"' + 'Invalid value {"id":"someId"}, excess properties: ["id"]' ); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts index c93983e499fb5..811d4531b86a7 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts @@ -5,6 +5,7 @@ */ import { extname } from 'path'; +import { Readable } from 'stream'; import { IRouter } from '../../../../../../../src/core/server'; @@ -12,7 +13,7 @@ import { TIMELINE_IMPORT_URL } from '../../../../common/constants'; import { SetupPlugins } from '../../../plugin'; import { ConfigType } from '../../../config'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { buildSiemResponse, transformError } from '../../detection_engine/routes/utils'; import { importTimelines } from './utils/import_timelines'; @@ -28,7 +29,7 @@ export const importTimelinesRoute = ( { path: `${TIMELINE_IMPORT_URL}`, validate: { - body: buildRouteValidation(ImportTimelinesPayloadSchemaRt), + body: buildRouteValidationWithExcess(ImportTimelinesPayloadSchemaRt), }, options: { tags: ['access:securitySolution'], @@ -60,7 +61,7 @@ export const importTimelinesRoute = ( const frameworkRequest = await buildFrameworkRequest(context, security, request); const res = await importTimelines( - file, + (file as unknown) as Readable, config.maxTimelineImportExportSize, frameworkRequest, isImmutable === 'true' diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/create_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/create_timelines_schema.ts index 241d266a14c78..8d542201f6108 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/create_timelines_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/create_timelines_schema.ts @@ -5,7 +5,11 @@ */ import * as rt from 'io-ts'; -import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; +import { + SavedTimelineRuntimeType, + TimelineStatusLiteralRt, + TimelineTypeLiteralRt, +} from '../../../../../common/types/timeline'; import { unionWithNullType } from '../../../../../common/utility_types'; export const createTimelineSchema = rt.intersection([ @@ -13,7 +17,11 @@ export const createTimelineSchema = rt.intersection([ timeline: SavedTimelineRuntimeType, }), rt.partial({ + status: unionWithNullType(TimelineStatusLiteralRt), timelineId: unionWithNullType(rt.string), + templateTimelineId: unionWithNullType(rt.string), + templateTimelineVersion: unionWithNullType(rt.number), + timelineType: unionWithNullType(TimelineTypeLiteralRt), version: unionWithNullType(rt.string), }), ]); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts index ce8eb93bdbdbd..4599d2bb571a2 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts @@ -11,8 +11,6 @@ export const exportTimelinesQuerySchema = rt.type({ file_name: rt.string, }); -export const exportTimelinesRequestBodySchema = unionWithNullType( - rt.partial({ - ids: unionWithNullType(rt.array(rt.string)), - }) -); +export const exportTimelinesRequestBodySchema = rt.partial({ + ids: unionWithNullType(rt.array(rt.string)), +}); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts index 65c956ed60440..2c6098bc75500 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/get_timeline_by_id_schema.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import * as rt from 'io-ts'; -import { unionWithNullType } from '../../../../../common/utility_types'; -export const getTimelineByIdSchemaQuery = unionWithNullType( - rt.partial({ - template_timeline_id: rt.string, - id: rt.string, - }) -); +export const getTimelineByIdSchemaQuery = rt.partial({ + template_timeline_id: rt.string, + id: rt.string, +}); export type GetTimelineByIdSchemaQuery = rt.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts index afce9d6cdcb24..89f3f9ddec1fc 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/import_timelines_schema.ts @@ -5,9 +5,6 @@ */ import * as rt from 'io-ts'; -import { Readable } from 'stream'; -import { either } from 'fp-ts/lib/Either'; - import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; import { eventNotes, globalNotes, pinnedEventIds } from './schemas'; @@ -28,16 +25,17 @@ export const ImportTimelinesSchemaRt = rt.intersection([ export type ImportTimelinesSchema = rt.TypeOf; -const ReadableRt = new rt.Type( - 'ReadableRt', - (u): u is Readable => u instanceof Readable, - (u, c) => - either.chain(rt.object.validate(u, c), (s) => { - const d = s as Readable; - return d.readable ? rt.success(d) : rt.failure(u, c); - }), - (a) => a -); +const ReadableRt = rt.partial({ + _maxListeners: rt.unknown, + _readableState: rt.unknown, + _read: rt.unknown, + readable: rt.boolean, + _events: rt.unknown, + _eventsCount: rt.number, + _data: rt.unknown, + _position: rt.number, + _encoding: rt.string, +}); const booleanInString = rt.union([rt.literal('true'), rt.literal('false')]); @@ -46,9 +44,14 @@ export const ImportTimelinesPayloadSchemaRt = rt.intersection([ file: rt.intersection([ ReadableRt, rt.type({ - hapi: rt.type({ filename: rt.string }), + hapi: rt.type({ + filename: rt.string, + headers: rt.unknown, + }), }), ]), }), rt.partial({ isImmutable: booleanInString }), ]); + +export type ImportTimelinesPayloadSchema = rt.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts index 07ce9a7336d4d..6b8ceea80c31a 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts @@ -9,7 +9,7 @@ import { IRouter } from '../../../../../../../src/core/server'; import { TIMELINE_URL } from '../../../../common/constants'; import { SetupPlugins } from '../../../plugin'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { buildRouteValidationWithExcess } from '../../../utils/build_validation/route_validation'; import { ConfigType } from '../../..'; import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; @@ -28,7 +28,7 @@ export const updateTimelinesRoute = ( { path: TIMELINE_URL, validate: { - body: buildRouteValidation(updateTimelineSchema), + body: buildRouteValidationWithExcess(updateTimelineSchema), }, options: { tags: ['access:securitySolution'], diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts index fc25f1a48194e..9a3dbf365e026 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts @@ -10,7 +10,7 @@ import { Readable } from 'stream'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { createListStream } from '../../../../../../../../src/legacy/utils'; +import { createListStream } from '../../../../../../../../src/core/server/utils'; import { SetupPlugins } from '../../../../plugin'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts index f62f02cc7bba9..a19b18e7d89b1 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/import_timelines.ts @@ -21,7 +21,7 @@ import { createBulkErrorObject, BulkError } from '../../../detection_engine/rout import { createTimelines } from './create_timelines'; import { FrameworkRequest } from '../../../framework'; import { createTimelinesStreamFromNdJson } from '../../create_timelines_stream_from_ndjson'; -import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils'; +import { createPromiseFromStreams } from '../../../../../../../../src/core/server/utils'; import { getTupleDuplicateErrorsAndUniqueTimeline } from './get_timelines_from_stream'; import { CompareTimelinesStatus } from './compare_timelines_status'; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.test.ts index 66f16db01a508..c63978a1f046e 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.test.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/install_prepacked_timelines.test.ts @@ -5,7 +5,7 @@ */ import { join, resolve } from 'path'; -import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; +import { createPromiseFromStreams } from '../../../../../../../../src/core/server/utils'; import { SecurityPluginSetup } from '../../../../../../security/server'; import { FrameworkRequest } from '../../../framework'; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 38120bf42fbba..24cf1f8746d89 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -61,6 +61,7 @@ import { initUsageCollectors } from './usage'; import { AppRequestContext } from './types'; import { registerTrustedAppsRoutes } from './endpoint/routes/trusted_apps'; import { securitySolutionSearchStrategyProvider } from './search_strategy/security_solution'; +import { securitySolutionTimelineSearchStrategyProvider } from './search_strategy/timeline'; export interface SetupPlugins { alerts: AlertingSetup; @@ -271,10 +272,17 @@ export class Plugin implements IPlugin { const securitySolutionSearchStrategy = securitySolutionSearchStrategyProvider(depsStart.data); + const securitySolutionTimelineSearchStrategy = securitySolutionTimelineSearchStrategyProvider( + depsStart.data + ); plugins.data.search.registerSearchStrategy( 'securitySolutionSearchStrategy', securitySolutionSearchStrategy ); + plugins.data.search.registerSearchStrategy( + 'securitySolutionTimelineSearchStrategy', + securitySolutionTimelineSearchStrategy + ); }); return {}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts b/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts new file mode 100644 index 0000000000000..4da319d5b1e42 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts @@ -0,0 +1,8 @@ +/* + * 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 const toArray = (value: T | T[] | null) => + Array.isArray(value) ? value : value == null ? [] : [value]; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts index 5c29d2747f68d..3550824028478 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts @@ -5,8 +5,8 @@ */ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; +import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostsEdges } from '../../../../../../common/search_strategy/security_solution/hosts'; -import { hostFieldsMap } from '../../../../../lib/ecs_fields'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts index ea1b896452c4e..93390c314a637 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts @@ -11,7 +11,7 @@ import { HostsRequestOptions, SortField, HostsFields, -} from '../../../../../../common/search_strategy/security_solution'; +} from '../../../../../../common/search_strategy'; import { createQueryFilterClauses } from '../../../../../utils/build_query'; import { assertUnreachable } from '../../../../../../common/utility_types'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts new file mode 100644 index 0000000000000..df300c85e300f --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts @@ -0,0 +1,120 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; + +import { HostAuthenticationsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts/authentications'; +import { sourceFieldsMap, hostFieldsMap } from '../../../../../../../common/ecs/ecs_fields'; + +import { createQueryFilterClauses } from '../../../../../../utils/build_query'; +import { reduceFields } from '../../../../../../utils/build_query/reduce_fields'; + +import { authenticationFields } from '../helpers'; +import { extendMap } from '../../../../../../../common/ecs/ecs_fields/extend_map'; + +export const auditdFieldsMap: Readonly> = { + latest: '@timestamp', + 'lastSuccess.timestamp': 'lastSuccess.@timestamp', + 'lastFailure.timestamp': 'lastFailure.@timestamp', + ...{ ...extendMap('lastSuccess', sourceFieldsMap) }, + ...{ ...extendMap('lastSuccess', hostFieldsMap) }, + ...{ ...extendMap('lastFailure', sourceFieldsMap) }, + ...{ ...extendMap('lastFailure', hostFieldsMap) }, +}; + +export const buildQuery = ({ + filterQuery, + timerange: { from, to }, + pagination: { querySize }, + defaultIndex, + docValueFields, +}: HostAuthenticationsRequestOptions) => { + const esFields = reduceFields(authenticationFields, { ...hostFieldsMap, ...sourceFieldsMap }); + + const filter = [ + ...createQueryFilterClauses(filterQuery), + { term: { 'event.category': 'authentication' } }, + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const agg = { + user_count: { + cardinality: { + field: 'user.name', + }, + }, + }; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggregations: { + ...agg, + group_by_users: { + terms: { + size: querySize, + field: 'user.name', + order: [{ 'successes.doc_count': 'desc' }, { 'failures.doc_count': 'desc' }], + }, + aggs: { + failures: { + filter: { + term: { + 'event.outcome': 'failure', + }, + }, + aggs: { + lastFailure: { + top_hits: { + size: 1, + _source: esFields, + sort: [{ '@timestamp': { order: 'desc' } }], + }, + }, + }, + }, + successes: { + filter: { + term: { + 'event.outcome': 'success', + }, + }, + aggs: { + lastSuccess: { + top_hits: { + size: 1, + _source: esFields, + sort: [{ '@timestamp': { order: 'desc' } }], + }, + }, + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + }, + track_total_hits: false, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts new file mode 100644 index 0000000000000..c6b68bd1c0762 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.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 { get, getOr } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; +import { mergeFieldsWithHit } from '../../../../../utils/build_query'; +import { toArray } from '../../../../helpers/to_array'; +import { + AuthenticationsEdges, + AuthenticationHit, + AuthenticationBucket, + FactoryQueryTypes, + StrategyResponseType, +} from '../../../../../../common/search_strategy/security_solution'; + +export const authenticationFields = [ + '_id', + 'failures', + 'successes', + 'user.name', + 'lastSuccess.timestamp', + 'lastSuccess.source.ip', + 'lastSuccess.host.id', + 'lastSuccess.host.name', + 'lastFailure.timestamp', + 'lastFailure.source.ip', + 'lastFailure.host.id', + 'lastFailure.host.name', +]; + +export const formatAuthenticationData = ( + hit: AuthenticationHit, + fieldMap: Readonly> +): AuthenticationsEdges => + authenticationFields.reduce( + (flattenedFields, fieldName) => { + if (hit.cursor) { + flattenedFields.cursor.value = hit.cursor; + } + flattenedFields.node = { + ...flattenedFields.node, + ...{ + _id: hit._id, + user: { name: [hit.user] }, + failures: hit.failures, + successes: hit.successes, + }, + }; + const mergedResult = mergeFieldsWithHit(fieldName, flattenedFields, fieldMap, hit); + const fieldPath = `node.${fieldName}`; + const fieldValue = get(fieldPath, mergedResult); + + return set(fieldPath, toArray(fieldValue), mergedResult); + }, + { + node: { + failures: 0, + successes: 0, + _id: '', + user: { + name: [''], + }, + }, + cursor: { + value: '', + tiebreaker: null, + }, + } + ); + +export const getHits = (response: StrategyResponseType) => + getOr([], 'aggregations.group_by_users.buckets', response.rawResponse).map( + (bucket: AuthenticationBucket) => ({ + _id: getOr( + `${bucket.key}+${bucket.doc_count}`, + 'failures.lastFailure.hits.hits[0].id', + bucket + ), + _source: { + lastSuccess: getOr(null, 'successes.lastSuccess.hits.hits[0]._source', bucket), + lastFailure: getOr(null, 'failures.lastFailure.hits.hits[0]._source', bucket), + }, + user: bucket.key, + failures: bucket.failures.doc_count, + successes: bucket.successes.doc_count, + }) + ); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx new file mode 100644 index 0000000000000..ded9a7917d921 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx @@ -0,0 +1,64 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + HostsQueries, + AuthenticationsEdges, + HostAuthenticationsRequestOptions, + HostAuthenticationsStrategyResponse, + AuthenticationHit, +} from '../../../../../../common/search_strategy/security_solution/hosts'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; +import { auditdFieldsMap, buildQuery as buildAuthenticationQuery } from './dsl/query.dsl'; +import { formatAuthenticationData, getHits } from './helpers'; + +export const authentications: SecuritySolutionFactory = { + buildDsl: (options: HostAuthenticationsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + + return buildAuthenticationQuery(options); + }, + parse: async ( + options: HostAuthenticationsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.user_count.value', response.rawResponse); + + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const hits: AuthenticationHit[] = getHits(response); + const authenticationEdges: AuthenticationsEdges[] = hits.map((hit) => + formatAuthenticationData(hit, auditdFieldsMap) + ); + + const edges = authenticationEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildAuthenticationQuery(options))], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + + return { + ...response, + inspect, + edges, + totalCount, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts index a7ec822839d21..56f7aec2327a5 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts @@ -5,14 +5,16 @@ */ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; +import { hostFieldsMap } from '../../../../../common/ecs/ecs_fields'; import { HostsEdges, HostItem, } from '../../../../../common/search_strategy/security_solution/hosts'; -import { hostFieldsMap } from '../../../../lib/ecs_fields'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../lib/hosts/types'; +import { toArray } from '../../../helpers/to_array'; + const hostsFields = ['_id', 'lastSeen', 'host.id', 'host.name', 'host.os.name', 'host.os.version']; export const formatHostEdgesData = (bucket: HostAggEsItem): HostsEdges => @@ -23,11 +25,7 @@ export const formatHostEdgesData = (bucket: HostAggEsItem): HostsEdges => flattenedFields.cursor.value = hostId || ''; const fieldValue = getHostFieldValue(fieldName, bucket); if (fieldValue != null) { - return set( - `node.${fieldName}`, - Array.isArray(fieldValue) ? fieldValue : [fieldValue], - flattenedFields - ); + return set(`node.${fieldName}`, toArray(fieldValue), flattenedFields); } return flattenedFields; }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts index 34676fc1932fe..38d81c229ac5f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts @@ -4,16 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FactoryQueryTypes } from '../../../../../common/search_strategy/security_solution'; -import { HostsQueries } from '../../../../../common/search_strategy/security_solution/hosts'; +import { + FactoryQueryTypes, + HostsQueries, +} from '../../../../../common/search_strategy/security_solution'; import { SecuritySolutionFactory } from '../types'; import { allHosts } from './all'; import { overviewHost } from './overview'; import { firstLastSeenHost } from './last_first_seen'; +import { uncommonProcesses } from './uncommon_processes'; +import { authentications } from './authentications'; export const hostsFactory: Record> = { [HostsQueries.hosts]: allHosts, [HostsQueries.hostOverview]: overviewHost, [HostsQueries.firstLastSeen]: firstLastSeenHost, + [HostsQueries.uncommonProcesses]: uncommonProcesses, + [HostsQueries.authentications]: authentications, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts index c7b0d8acc8782..ed705e7f6ad56 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts @@ -5,8 +5,8 @@ */ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; +import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostItem } from '../../../../../../common/search_strategy/security_solution/hosts'; -import { hostFieldsMap } from '../../../../../lib/ecs_fields'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts index 913bc90df04be..85cc87414c38e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts @@ -5,8 +5,8 @@ */ import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { cloudFieldsMap, hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostOverviewRequestOptions } from '../../../../../../common/search_strategy/security_solution'; -import { cloudFieldsMap, hostFieldsMap } from '../../../../../lib/ecs_fields'; import { buildFieldsTermAggregation } from '../../../../../lib/hosts/helpers'; import { reduceFields } from '../../../../../utils/build_query/reduce_fields'; import { HOST_FIELDS } from './helpers'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts new file mode 100644 index 0000000000000..2e2d889dda116 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts @@ -0,0 +1,226 @@ +/* + * 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 { createQueryFilterClauses } from '../../../../../../utils/build_query'; +import { reduceFields } from '../../../../../../utils/build_query/reduce_fields'; +import { + hostFieldsMap, + processFieldsMap, + userFieldsMap, +} from '../../../../../../../common/ecs/ecs_fields'; +import { RequestOptionsPaginated } from '../../../../../../../common/search_strategy/security_solution'; +import { uncommonProcessesFields } from '../helpers'; + +export const buildQuery = ({ + defaultIndex, + filterQuery, + pagination: { querySize }, + timerange: { from, to }, +}: RequestOptionsPaginated) => { + const processUserFields = reduceFields(uncommonProcessesFields, { + ...processFieldsMap, + ...userFieldsMap, + }); + const hostFields = reduceFields(uncommonProcessesFields, hostFieldsMap); + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const agg = { + process_count: { + cardinality: { + field: 'process.name', + }, + }, + }; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + aggregations: { + ...agg, + group_by_process: { + terms: { + size: querySize, + field: 'process.name', + order: [ + { + host_count: 'asc', + }, + { + _count: 'asc', + }, + { + _key: 'asc', + }, + ], + }, + aggregations: { + process: { + top_hits: { + size: 1, + sort: [{ '@timestamp': { order: 'desc' } }], + _source: processUserFields, + }, + }, + host_count: { + cardinality: { + field: 'host.name', + }, + }, + hosts: { + terms: { + field: 'host.name', + }, + aggregations: { + host: { + top_hits: { + size: 1, + _source: hostFields, + }, + }, + }, + }, + }, + }, + }, + query: { + bool: { + should: [ + { + bool: { + filter: [ + { + term: { + 'agent.type': 'auditbeat', + }, + }, + { + term: { + 'event.module': 'auditd', + }, + }, + { + term: { + 'event.action': 'executed', + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'agent.type': 'auditbeat', + }, + }, + { + term: { + 'event.module': 'system', + }, + }, + { + term: { + 'event.dataset': 'process', + }, + }, + { + term: { + 'event.action': 'process_started', + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'agent.type': 'winlogbeat', + }, + }, + { + term: { + 'event.code': '4688', + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'winlog.event_id': 1, + }, + }, + { + term: { + 'winlog.channel': 'Microsoft-Windows-Sysmon/Operational', + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'event.type': 'process_start', + }, + }, + { + term: { + 'event.category': 'process', + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'event.category': 'process', + }, + }, + { + term: { + 'event.type': 'start', + }, + }, + ], + }, + }, + ], + minimum_should_match: 1, + filter, + }, + }, + }, + size: 0, + track_total_hits: false, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts new file mode 100644 index 0000000000000..5c3d76175b7e4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts @@ -0,0 +1,94 @@ +/* + * 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, getOr } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; + +import { mergeFieldsWithHit } from '../../../../../utils/build_query'; +import { + ProcessHits, + UncommonProcessesEdges, + UncommonProcessHit, +} from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; +import { toArray } from '../../../../helpers/to_array'; +import { HostHits } from '../../../../../../common/search_strategy'; + +export const uncommonProcessesFields = [ + '_id', + 'instances', + 'process.args', + 'process.name', + 'user.id', + 'user.name', + 'hosts.name', +]; + +export const getHits = (buckets: readonly UncommonProcessBucket[]): readonly UncommonProcessHit[] => + buckets.map((bucket: Readonly) => ({ + _id: bucket.process.hits.hits[0]._id, + _index: bucket.process.hits.hits[0]._index, + _type: bucket.process.hits.hits[0]._type, + _score: bucket.process.hits.hits[0]._score, + _source: bucket.process.hits.hits[0]._source, + sort: bucket.process.hits.hits[0].sort, + cursor: bucket.process.hits.hits[0].cursor, + total: bucket.process.hits.total, + host: getHosts(bucket.hosts.buckets), + })); + +export interface UncommonProcessBucket { + key: string; + hosts: { + buckets: Array<{ key: string; host: HostHits }>; + }; + process: ProcessHits; +} + +export const getHosts = (buckets: ReadonlyArray<{ key: string; host: HostHits }>) => + buckets.map((bucket) => { + const source = get('host.hits.hits[0]._source', bucket); + return { + id: [bucket.key], + name: get('host.name', source), + }; + }); + +export const formatUncommonProcessesData = ( + fields: readonly string[], + hit: UncommonProcessHit, + fieldMap: Readonly> +): UncommonProcessesEdges => + fields.reduce( + (flattenedFields, fieldName) => { + flattenedFields.node._id = hit._id; + flattenedFields.node.instances = getOr(0, 'total.value', hit); + flattenedFields.node.hosts = hit.host; + + if (hit.cursor) { + flattenedFields.cursor.value = hit.cursor; + } + + const mergedResult = mergeFieldsWithHit(fieldName, flattenedFields, fieldMap, hit); + let fieldPath = `node.${fieldName}`; + let fieldValue = get(fieldPath, mergedResult); + if (fieldPath === 'node.hosts.name') { + fieldPath = `node.hosts.0.name`; + fieldValue = get(fieldPath, mergedResult); + } + return set(fieldPath, toArray(fieldValue), mergedResult); + }, + { + node: { + _id: '', + instances: 0, + process: {}, + hosts: [], + }, + cursor: { + value: '', + tiebreaker: null, + }, + } + ); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts new file mode 100644 index 0000000000000..fcc76eebe4cf5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { HostsQueries } from '../../../../../../common/search_strategy/security_solution'; +import { processFieldsMap, userFieldsMap } from '../../../../../../common/ecs/ecs_fields'; +import { + HostUncommonProcessesRequestOptions, + HostUncommonProcessesStrategyResponse, +} from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; + +import { SecuritySolutionFactory } from '../../types'; +import { buildQuery } from './dsl/query.dsl'; +import { formatUncommonProcessesData, getHits, uncommonProcessesFields } from './helpers'; + +export const uncommonProcesses: SecuritySolutionFactory = { + buildDsl: (options: HostUncommonProcessesRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildQuery(options); + }, + parse: async ( + options: HostUncommonProcessesRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.process_count.value', response.rawResponse); + const buckets = getOr([], 'aggregations.group_by_process.buckets', response.rawResponse); + const hits = getHits(buckets); + + const uncommonProcessesEdges = hits.map((hit) => + formatUncommonProcessesData(uncommonProcessesFields, hit, { + ...processFieldsMap, + ...userFieldsMap, + }) + ); + + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = uncommonProcessesEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildQuery(options))], + response: [inspectStringifyObject(response)], + }; + + const showMorePagesIndicator = totalCount > fakeTotalCount; + return { + ...response, + edges, + inspect, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + totalCount, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/helpers.ts new file mode 100644 index 0000000000000..a7fba087b87ed --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/helpers.ts @@ -0,0 +1,18 @@ +/* + * 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 { assertUnreachable } from '../../../../../common/utility_types'; +import { FlowTargetSourceDest } from '../../../../../common/search_strategy/security_solution/network'; + +export const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSourceDest => { + switch (flowTarget) { + case FlowTargetSourceDest.source: + return FlowTargetSourceDest.destination; + case FlowTargetSourceDest.destination: + return FlowTargetSourceDest.source; + } + assertUnreachable(flowTarget); +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/helpers.ts new file mode 100644 index 0000000000000..b8a28441337c7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/helpers.ts @@ -0,0 +1,34 @@ +/* + * 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, getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + NetworkHttpBuckets, + NetworkHttpEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +export const getHttpEdges = (response: IEsSearchResponse): NetworkHttpEdges[] => + formatHttpEdges(getOr([], `aggregations.url.buckets`, response.rawResponse)); + +const formatHttpEdges = (buckets: NetworkHttpBuckets[]): NetworkHttpEdges[] => + buckets.map((bucket: NetworkHttpBuckets) => ({ + node: { + _id: bucket.key, + domains: bucket.domains.buckets.map(({ key }) => key), + methods: bucket.methods.buckets.map(({ key }) => key), + statuses: bucket.status.buckets.map(({ key }) => `${key}`), + lastHost: get('source.hits.hits[0]._source.host.name', bucket), + lastSourceIp: get('source.hits.hits[0]._source.source.ip', bucket), + path: bucket.key, + requestCount: bucket.doc_count, + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts new file mode 100644 index 0000000000000..c0205ccce63cc --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/index.ts @@ -0,0 +1,58 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + NetworkHttpStrategyResponse, + NetworkQueries, + NetworkHttpRequestOptions, + NetworkHttpEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; + +import { getHttpEdges } from './helpers'; +import { buildHttpQuery } from './query.http_network.dsl'; + +export const networkHttp: SecuritySolutionFactory = { + buildDsl: (options: NetworkHttpRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildHttpQuery(options); + }, + parse: async ( + options: NetworkHttpRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.http_count.value', response.rawResponse); + const networkHttpEdges: NetworkHttpEdges[] = getHttpEdges(response); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = networkHttpEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildHttpQuery(options))], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + + return { + ...response, + edges, + inspect, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + totalCount, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.ts new file mode 100644 index 0000000000000..feffe7f70afd9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/http/query.http_network.dsl.ts @@ -0,0 +1,113 @@ +/* + * 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 { createQueryFilterClauses } from '../../../../../utils/build_query'; + +import { NetworkHttpRequestOptions, SortField } from '../../../../../../common/search_strategy'; + +const getCountAgg = () => ({ + http_count: { + cardinality: { + field: 'url.path', + }, + }, +}); + +export const buildHttpQuery = ({ + defaultIndex, + filterQuery, + sort, + pagination: { querySize }, + timerange: { from, to }, + ip, +}: NetworkHttpRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, + { exists: { field: 'http.request.method' } }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + aggregations: { + ...getCountAgg(), + ...getHttpAggs(sort, querySize), + }, + query: { + bool: ip + ? { + filter, + should: [ + { + term: { + 'source.ip': ip, + }, + }, + { + term: { + 'destination.ip': ip, + }, + }, + ], + minimum_should_match: 1, + } + : { + filter, + }, + }, + }, + size: 0, + track_total_hits: false, + }; + return dslQuery; +}; + +const getHttpAggs = (sortField: SortField, querySize: number) => ({ + url: { + terms: { + field: `url.path`, + size: querySize, + order: { + _count: sortField.direction, + }, + }, + aggs: { + methods: { + terms: { + field: 'http.request.method', + size: 4, + }, + }, + domains: { + terms: { + field: 'url.domain', + size: 4, + }, + }, + status: { + terms: { + field: 'http.response.status_code', + size: 4, + }, + }, + source: { + top_hits: { + size: 1, + _source: { + includes: ['host.name', 'source.ip'], + }, + }, + }, + }, + }, +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts index 2c21d9741d648..9e73312bdb8e1 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts @@ -4,12 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FactoryQueryTypes } from '../../../../../common/search_strategy/security_solution'; -import { NetworkQueries } from '../../../../../common/search_strategy/security_solution/network'; +import { + FactoryQueryTypes, + NetworkQueries, +} from '../../../../../common/search_strategy/security_solution'; import { SecuritySolutionFactory } from '../types'; +import { networkHttp } from './http'; import { networkTls } from './tls'; +import { networkTopCountries } from './top_countries'; export const networkFactory: Record> = { + [NetworkQueries.http]: networkHttp, [NetworkQueries.tls]: networkTls, + [NetworkQueries.topCountries]: networkTopCountries, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts index eb4e25c29e3a1..6e5ba0674a0e7 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts @@ -12,7 +12,7 @@ import { SortField, Direction, TlsFields, -} from '../../../../../../common/search_strategy/security_solution'; +} from '../../../../../../common/search_strategy'; const getAggs = (querySize: number, sort: SortField) => ({ count: { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/helpers.ts new file mode 100644 index 0000000000000..a8972c3d89f36 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/helpers.ts @@ -0,0 +1,53 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + NetworkTopCountriesBuckets, + NetworkTopCountriesEdges, + NetworkTopCountriesRequestOptions, + FlowTargetSourceDest, +} from '../../../../../../common/search_strategy/security_solution/network'; +import { getOppositeField } from '../helpers'; + +export const getTopCountriesEdges = ( + response: IEsSearchResponse, + options: NetworkTopCountriesRequestOptions +): NetworkTopCountriesEdges[] => + formatTopCountriesEdges( + getOr([], `aggregations.${options.flowTarget}.buckets`, response.rawResponse), + options.flowTarget + ); + +export const formatTopCountriesEdges = ( + buckets: NetworkTopCountriesBuckets[], + flowTarget: FlowTargetSourceDest +): NetworkTopCountriesEdges[] => + buckets.map((bucket: NetworkTopCountriesBuckets) => ({ + node: { + _id: bucket.key, + [flowTarget]: { + country: bucket.key, + flows: getOr(0, 'flows.value', bucket), + [`${getOppositeField(flowTarget)}_ips`]: getOr( + 0, + `${getOppositeField(flowTarget)}_ips.value`, + bucket + ), + [`${flowTarget}_ips`]: getOr(0, `${flowTarget}_ips.value`, bucket), + }, + network: { + bytes_in: getOr(0, 'bytes_in.value', bucket), + bytes_out: getOr(0, 'bytes_out.value', bucket), + }, + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + })); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.ts new file mode 100644 index 0000000000000..5b0ced06f2ee9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/index.ts @@ -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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + NetworkTopCountriesStrategyResponse, + NetworkQueries, + NetworkTopCountriesRequestOptions, + NetworkTopCountriesEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; + +import { getTopCountriesEdges } from './helpers'; +import { buildTopCountriesQuery } from './query.top_countries_network.dsl'; + +export const networkTopCountries: SecuritySolutionFactory = { + buildDsl: (options: NetworkTopCountriesRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildTopCountriesQuery(options); + }, + parse: async ( + options: NetworkTopCountriesRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.top_countries_count.value', response.rawResponse); + const networkTopCountriesEdges: NetworkTopCountriesEdges[] = getTopCountriesEdges( + response, + options + ); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = networkTopCountriesEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildTopCountriesQuery(options))], + response: [inspectStringifyObject(response)], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + + return { + ...response, + edges, + inspect, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + totalCount, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/query.top_countries_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/query.top_countries_network.dsl.ts new file mode 100644 index 0000000000000..4f4b347e4db02 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_countries/query.top_countries_network.dsl.ts @@ -0,0 +1,152 @@ +/* + * 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 { createQueryFilterClauses } from '../../../../../utils/build_query'; +import { assertUnreachable } from '../../../../../../common/utility_types'; +import { + Direction, + FlowTargetSourceDest, + NetworkTopTablesFields, + NetworkTopCountriesRequestOptions, + SortField, +} from '../../../../../../common/search_strategy'; + +const getCountAgg = (flowTarget: FlowTargetSourceDest) => ({ + top_countries_count: { + cardinality: { + field: `${flowTarget}.geo.country_iso_code`, + }, + }, +}); + +export const buildTopCountriesQuery = ({ + defaultIndex, + filterQuery, + flowTarget, + sort, + pagination: { querySize }, + timerange: { from, to }, + ip, +}: NetworkTopCountriesRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + aggregations: { + ...getCountAgg(flowTarget), + ...getFlowTargetAggs(sort, flowTarget, querySize), + }, + query: { + bool: ip + ? { + filter, + should: [ + { + term: { + [`${getOppositeField(flowTarget)}.ip`]: ip, + }, + }, + ], + minimum_should_match: 1, + } + : { + filter, + }, + }, + }, + size: 0, + track_total_hits: false, + }; + return dslQuery; +}; + +const getFlowTargetAggs = ( + sort: SortField, + flowTarget: FlowTargetSourceDest, + querySize: number +) => ({ + [flowTarget]: { + terms: { + field: `${flowTarget}.geo.country_iso_code`, + size: querySize, + order: { + ...getQueryOrder(sort), + }, + }, + aggs: { + bytes_in: { + sum: { + field: `${getOppositeField(flowTarget)}.bytes`, + }, + }, + bytes_out: { + sum: { + field: `${flowTarget}.bytes`, + }, + }, + flows: { + cardinality: { + field: 'network.community_id', + }, + }, + source_ips: { + cardinality: { + field: 'source.ip', + }, + }, + destination_ips: { + cardinality: { + field: 'destination.ip', + }, + }, + }, + }, +}); + +export const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSourceDest => { + switch (flowTarget) { + case FlowTargetSourceDest.source: + return FlowTargetSourceDest.destination; + case FlowTargetSourceDest.destination: + return FlowTargetSourceDest.source; + } + assertUnreachable(flowTarget); +}; + +type QueryOrder = + | { bytes_in: Direction } + | { bytes_out: Direction } + | { flows: Direction } + | { destination_ips: Direction } + | { source_ips: Direction }; + +const getQueryOrder = ( + networkTopCountriesSortField: SortField +): QueryOrder => { + switch (networkTopCountriesSortField.field) { + case NetworkTopTablesFields.bytes_in: + return { bytes_in: networkTopCountriesSortField.direction }; + case NetworkTopTablesFields.bytes_out: + return { bytes_out: networkTopCountriesSortField.direction }; + case NetworkTopTablesFields.flows: + return { flows: networkTopCountriesSortField.direction }; + case NetworkTopTablesFields.destination_ips: + return { destination_ips: networkTopCountriesSortField.direction }; + case NetworkTopTablesFields.source_ips: + return { source_ips: networkTopCountriesSortField.direction }; + } + assertUnreachable(networkTopCountriesSortField.field); +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/details/helpers.ts new file mode 100644 index 0000000000000..b772dec773dce --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/details/helpers.ts @@ -0,0 +1,55 @@ +/* + * 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, isEmpty, isNumber, isObject, isString } from 'lodash/fp'; + +import { DetailItem } from '../../../../../common/search_strategy/timeline'; +import { baseCategoryFields } from '../../../../utils/beat_schema/8.0.0'; + +export const getFieldCategory = (field: string): string => { + const fieldCategory = field.split('.')[0]; + if (!isEmpty(fieldCategory) && baseCategoryFields.includes(fieldCategory)) { + return 'base'; + } + return fieldCategory; +}; + +export const getDataFromHits = ( + sources: EventSource, + category?: string, + path?: string +): DetailItem[] => + Object.keys(sources).reduce((accumulator, source) => { + const item: EventSource = get(source, sources); + if (Array.isArray(item) || isString(item) || isNumber(item)) { + const field = path ? `${path}.${source}` : source; + const fieldCategory = getFieldCategory(field); + + return [ + ...accumulator, + { + category: fieldCategory, + field, + values: Array.isArray(item) + ? item.map((value) => { + if (isObject(value)) { + return JSON.stringify(value); + } + + return value; + }) + : [item], + originalValue: item, + } as DetailItem, + ]; + } else if (isObject(item)) { + return [ + ...accumulator, + ...getDataFromHits(item, category || source, path ? `${path}.${source}` : source), + ]; + } + return accumulator; + }, []); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/details/index.ts new file mode 100644 index 0000000000000..e1fabe2b4d586 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/details/index.ts @@ -0,0 +1,44 @@ +/* + * 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 { getOr, merge } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { + TimelineQueries, + TimelineDetailsStrategyResponse, + TimelineDetailsRequestOptions, +} from '../../../../../common/search_strategy/timeline'; +import { inspectStringifyObject } from '../../../../utils/build_query'; +import { SecuritySolutionTimelineFactory } from '../types'; +import { buildTimelineDetailsQuery } from './query.timeline_details.dsl'; +import { getDataFromHits } from './helpers'; + +export const timelineDetails: SecuritySolutionTimelineFactory = { + buildDsl: (options: TimelineDetailsRequestOptions) => { + const { indexName, eventId, docValueFields = [] } = options; + return buildTimelineDetailsQuery(indexName, eventId, docValueFields); + }, + parse: async ( + options: TimelineDetailsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { indexName, eventId, docValueFields = [] } = options; + const sourceData = getOr({}, 'hits.hits.0._source', response.rawResponse); + const hitsData = getOr({}, 'hits.hits.0', response.rawResponse); + delete hitsData._source; + const inspect = { + dsl: [inspectStringifyObject(buildTimelineDetailsQuery(indexName, eventId, docValueFields))], + }; + const data = getDataFromHits(merge(sourceData, hitsData)); + + return { + ...response, + data, + inspect, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/details/query.timeline_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/details/query.timeline_details.dsl.ts new file mode 100644 index 0000000000000..4f003c1c27ef2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/details/query.timeline_details.dsl.ts @@ -0,0 +1,26 @@ +/* + * 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 { DocValueFields } from '../../../../../common/search_strategy'; + +export const buildTimelineDetailsQuery = ( + indexName: string, + id: string, + docValueFields: DocValueFields[] +) => ({ + allowNoIndices: true, + index: indexName, + ignoreUnavailable: true, + body: { + docvalue_fields: docValueFields, + query: { + terms: { + _id: [id], + }, + }, + }, + size: 1, +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/index.ts new file mode 100644 index 0000000000000..aa4cdaeedb131 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/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. + */ + +import { + TimelineFactoryQueryTypes, + TimelineQueries, +} from '../../../../common/search_strategy/timeline'; + +import { timelineDetails } from './details'; +import { SecuritySolutionTimelineFactory } from './types'; + +export const securitySolutionTimelineFactory: Record< + TimelineFactoryQueryTypes, + SecuritySolutionTimelineFactory +> = { + [TimelineQueries.details]: timelineDetails, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/types.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/types.ts new file mode 100644 index 0000000000000..55eddf64b68ff --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/types.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. + */ + +import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; +import { + TimelineFactoryQueryTypes, + TimelineStrategyRequestType, + TimelineStrategyResponseType, +} from '../../../../common/search_strategy/timeline'; + +export interface SecuritySolutionTimelineFactory { + buildDsl: (options: TimelineStrategyRequestType) => unknown; + parse: ( + options: TimelineStrategyRequestType, + response: IEsSearchResponse + ) => Promise>; +} diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts new file mode 100644 index 0000000000000..6d8505211123b --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts @@ -0,0 +1,38 @@ +/* + * 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 { ISearchStrategy, PluginStart } from '../../../../../../src/plugins/data/server'; +import { + TimelineFactoryQueryTypes, + TimelineStrategyResponseType, + TimelineStrategyRequestType, +} from '../../../common/search_strategy/timeline'; +import { securitySolutionTimelineFactory } from './factory'; +import { SecuritySolutionTimelineFactory } from './factory/types'; + +export const securitySolutionTimelineSearchStrategyProvider = ( + data: PluginStart +): ISearchStrategy, TimelineStrategyResponseType> => { + const es = data.search.getSearchStrategy('es'); + + return { + search: async (context, request, options) => { + if (request.factoryQueryType == null) { + throw new Error('factoryQueryType is required'); + } + const queryFactory: SecuritySolutionTimelineFactory = + securitySolutionTimelineFactory[request.factoryQueryType]; + const dsl = queryFactory.buildDsl(request); + const esSearchRes = await es.search(context, { ...request, params: dsl }, options); + return queryFactory.parse(request, esSearchRes); + }, + cancel: async (context, id) => { + if (es.cancel) { + es.cancel(context, id); + } + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index a6d4dc7a38e14..5cf17af2fa9c0 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -172,18 +172,12 @@ export const getMlJobsUsage = async (ml: MlPluginSetup | undefined): Promise module.jobs); - const jobs = await ml.jobServiceProvider(internalMlClient, fakeRequest).jobsSummary(); + const jobs = await ml.jobServiceProvider(fakeRequest).jobsSummary(); jobsUsage = jobs.filter(isSecurityJob).reduce((usage, job) => { const isElastic = moduleJobs.some((moduleJob) => moduleJob.id === job.id); diff --git a/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.test.ts b/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.test.ts index 9559e442e2159..ffc12d2bce261 100644 --- a/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.test.ts +++ b/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.test.ts @@ -3,84 +3,195 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { buildRouteValidation } from './route_validation'; import * as rt from 'io-ts'; import { RouteValidationResultFactory } from 'src/core/server'; -describe('buildRouteValidation', () => { - const schema = rt.exact( - rt.type({ - ids: rt.array(rt.string), - }) - ); - type Schema = rt.TypeOf; - - /** - * If your schema is using exact all the way down then the validation will - * catch any additional keys that should not be present within the validation - * when the route_validation uses the exact check. - */ - const deepSchema = rt.exact( - rt.type({ - topLevel: rt.exact( - rt.type({ - secondLevel: rt.exact( - rt.type({ - thirdLevel: rt.string, - }) - ), - }) - ), - }) - ); - type DeepSchema = rt.TypeOf; - - const validationResult: RouteValidationResultFactory = { - ok: jest.fn().mockImplementation((validatedInput) => validatedInput), - badRequest: jest.fn().mockImplementation((e) => e), - }; - - beforeEach(() => { - jest.clearAllMocks(); - }); +import { buildRouteValidation, buildRouteValidationWithExcess } from './route_validation'; - test('return validation error', () => { - const input: Omit & { id: string } = { id: 'someId' }; - const result = buildRouteValidation(schema)(input, validationResult); +describe('Route Validation with ', () => { + describe('buildRouteValidation', () => { + const schema = rt.exact( + rt.type({ + ids: rt.array(rt.string), + }) + ); + type Schema = rt.TypeOf; - expect(result).toEqual('Invalid value "undefined" supplied to "ids"'); - }); + /** + * If your schema is using exact all the way down then the validation will + * catch any additional keys that should not be present within the validation + * when the route_validation uses the exact check. + */ + const deepSchema = rt.exact( + rt.type({ + topLevel: rt.exact( + rt.type({ + secondLevel: rt.exact( + rt.type({ + thirdLevel: rt.string, + }) + ), + }) + ), + }) + ); + type DeepSchema = rt.TypeOf; - test('return validated input', () => { - const input: Schema = { ids: ['someId'] }; - const result = buildRouteValidation(schema)(input, validationResult); + const validationResult: RouteValidationResultFactory = { + ok: jest.fn().mockImplementation((validatedInput) => validatedInput), + badRequest: jest.fn().mockImplementation((e) => e), + }; - expect(result).toEqual(input); - }); + beforeEach(() => { + jest.clearAllMocks(); + }); - test('returns validation error if given extra keys on input for an array', () => { - const input: Schema & { somethingExtra: string } = { - ids: ['someId'], - somethingExtra: 'hello', - }; - const result = buildRouteValidation(schema)(input, validationResult); - expect(result).toEqual('invalid keys "somethingExtra"'); - }); + test('return validation error', () => { + const input: Omit & { id: string } = { id: 'someId' }; + const result = buildRouteValidation(schema)(input, validationResult); + + expect(result).toEqual('Invalid value "undefined" supplied to "ids"'); + }); + + test('return validated input', () => { + const input: Schema = { ids: ['someId'] }; + const result = buildRouteValidation(schema)(input, validationResult); + + expect(result).toEqual(input); + }); - test('return validation input for a deep 3rd level object', () => { - const input: DeepSchema = { topLevel: { secondLevel: { thirdLevel: 'hello' } } }; - const result = buildRouteValidation(deepSchema)(input, validationResult); - expect(result).toEqual(input); + test('returns validation error if given extra keys on input for an array', () => { + const input: Schema & { somethingExtra: string } = { + ids: ['someId'], + somethingExtra: 'hello', + }; + const result = buildRouteValidation(schema)(input, validationResult); + expect(result).toEqual('invalid keys "somethingExtra"'); + }); + + test('return validation input for a deep 3rd level object', () => { + const input: DeepSchema = { topLevel: { secondLevel: { thirdLevel: 'hello' } } }; + const result = buildRouteValidation(deepSchema)(input, validationResult); + expect(result).toEqual(input); + }); + + test('return validation error for a deep 3rd level object that has an extra key value of "somethingElse"', () => { + const input: DeepSchema & { + topLevel: { secondLevel: { thirdLevel: string; somethingElse: string } }; + } = { + topLevel: { secondLevel: { thirdLevel: 'hello', somethingElse: 'extraKey' } }, + }; + const result = buildRouteValidation(deepSchema)(input, validationResult); + expect(result).toEqual('invalid keys "somethingElse"'); + }); }); - test('return validation error for a deep 3rd level object that has an extra key value of "somethingElse"', () => { - const input: DeepSchema & { - topLevel: { secondLevel: { thirdLevel: string; somethingElse: string } }; - } = { - topLevel: { secondLevel: { thirdLevel: 'hello', somethingElse: 'extraKey' } }, + describe('buildRouteValidationwithExcess', () => { + const schema = rt.type({ + ids: rt.array(rt.string), + }); + type Schema = rt.TypeOf; + + /** + * If your schema is using exact all the way down then the validation will + * catch any additional keys that should not be present within the validation + * when the route_validation uses the exact check. + */ + const deepSchema = rt.type({ + topLevel: rt.type({ + secondLevel: rt.type({ + thirdLevel: rt.string, + }), + }), + }); + type DeepSchema = rt.TypeOf; + + const validationResult: RouteValidationResultFactory = { + ok: jest.fn().mockImplementation((validatedInput) => validatedInput), + badRequest: jest.fn().mockImplementation((e) => e), }; - const result = buildRouteValidation(deepSchema)(input, validationResult); - expect(result).toEqual('invalid keys "somethingElse"'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('return validation error', () => { + const input: Omit & { id: string } = { id: 'someId' }; + const result = buildRouteValidationWithExcess(schema)(input, validationResult); + + expect(result).toEqual('Invalid value {"id":"someId"}, excess properties: ["id"]'); + }); + + test('return validation error with intersection', () => { + const schemaI = rt.intersection([ + rt.type({ + ids: rt.array(rt.string), + }), + rt.partial({ + valid: rt.array(rt.string), + }), + ]); + type SchemaI = rt.TypeOf; + const input: Omit & { id: string } = { id: 'someId', valid: ['yes'] }; + const result = buildRouteValidationWithExcess(schemaI)(input, validationResult); + + expect(result).toEqual( + 'Invalid value {"id":"someId","valid":["yes"]}, excess properties: ["id"]' + ); + }); + + test('return NO validation error with a partial intersection', () => { + const schemaI = rt.intersection([ + rt.type({ + id: rt.array(rt.string), + }), + rt.partial({ + valid: rt.array(rt.string), + }), + ]); + const input = { id: ['someId'] }; + const result = buildRouteValidationWithExcess(schemaI)(input, validationResult); + + expect(result).toEqual({ id: ['someId'] }); + }); + + test('return validated input', () => { + const input: Schema = { ids: ['someId'] }; + const result = buildRouteValidationWithExcess(schema)(input, validationResult); + + expect(result).toEqual(input); + }); + + test('returns validation error if given extra keys on input for an array', () => { + const input: Schema & { somethingExtra: string } = { + ids: ['someId'], + somethingExtra: 'hello', + }; + const result = buildRouteValidationWithExcess(schema)(input, validationResult); + expect(result).toEqual( + 'Invalid value {"ids":["someId"],"somethingExtra":"hello"}, excess properties: ["somethingExtra"]' + ); + }); + + test('return validation input for a deep 3rd level object', () => { + const input: DeepSchema = { topLevel: { secondLevel: { thirdLevel: 'hello' } } }; + const result = buildRouteValidationWithExcess(deepSchema)(input, validationResult); + expect(result).toEqual(input); + }); + + test('return validation error for a deep 3rd level object that has an extra key value of "somethingElse"', () => { + const input: DeepSchema & { + topLevel: { secondLevel: { thirdLevel: string; somethingElse: string } }; + } = { + topLevel: { secondLevel: { thirdLevel: 'hello', somethingElse: 'extraKey' } }, + }; + const result = buildRouteValidationWithExcess(deepSchema)(input, validationResult); + expect(result).toEqual( + 'Invalid value {"topLevel":{"secondLevel":{"thirdLevel":"hello","somethingElse":"extraKey"}}}, excess properties: ["somethingElse"]' + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts b/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts index d7ab9affa6c1c..51f807d6aad81 100644 --- a/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts +++ b/x-pack/plugins/security_solution/server/utils/build_validation/route_validation.ts @@ -14,6 +14,7 @@ import { RouteValidationResultFactory, RouteValidationError, } from '../../../../../../src/core/server'; +import { excess, GenericIntersectionC } from '../runtime_types'; type RequestValidationResult = | { @@ -39,3 +40,20 @@ export const buildRouteValidation = >( (validatedInput: A) => validationResult.ok(validatedInput) ) ); + +export const buildRouteValidationWithExcess = < + T extends rt.InterfaceType | GenericIntersectionC | rt.PartialType, + A = rt.TypeOf +>( + schema: T +): RouteValidationFunction => ( + inputValue: unknown, + validationResult: RouteValidationResultFactory +) => + pipe( + excess(schema).decode(inputValue), + fold>( + (errors: rt.Errors) => validationResult.badRequest(formatErrors(errors).join()), + (validatedInput: A) => validationResult.ok(validatedInput) + ) + ); diff --git a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts index 0eb021bfe2a83..4446e82f99de4 100644 --- a/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts +++ b/x-pack/plugins/security_solution/server/utils/read_stream/create_stream_from_ndjson.ts @@ -16,7 +16,7 @@ import { ImportRulesSchema, } from '../../../common/detection_engine/schemas/request/import_rules_schema'; import { exactCheck } from '../../../common/exact_check'; -import { createMapStream, createFilterStream } from '../../../../../../src/legacy/utils/streams'; +import { createMapStream, createFilterStream } from '../../../../../../src/core/server/utils'; import { BadRequestError } from '../../lib/detection_engine/errors/bad_request_error'; export interface RulesObjectsExportResultDetails { diff --git a/x-pack/plugins/security_solution/server/utils/runtime_types.ts b/x-pack/plugins/security_solution/server/utils/runtime_types.ts new file mode 100644 index 0000000000000..7177cc5765f8a --- /dev/null +++ b/x-pack/plugins/security_solution/server/utils/runtime_types.ts @@ -0,0 +1,130 @@ +/* + * 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 { either, fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { failure } from 'io-ts/lib/PathReporter'; +import get from 'lodash/get'; + +type ErrorFactory = (message: string) => Error; + +export type GenericIntersectionC = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any, any]> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.IntersectionC<[any, any, any, any, any]>; + +export const createPlainError = (message: string) => new Error(message); + +export const throwErrors = (createError: ErrorFactory) => (errors: rt.Errors) => { + throw createError(failure(errors).join('\n')); +}; + +export const decodeOrThrow = ( + runtimeType: rt.Type, + createError: ErrorFactory = createPlainError +) => (inputValue: I) => + pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); + +const getProps = ( + codec: + | rt.HasProps + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | rt.RecordC + | GenericIntersectionC +): rt.Props | null => { + if (codec == null) { + return null; + } + switch (codec._tag) { + case 'DictionaryType': + if (codec.codomain.props != null) { + return codec.codomain.props; + } + const dTypes: rt.HasProps[] = codec.codomain.types; + return dTypes.reduce((props, type) => Object.assign(props, getProps(type)), {}); + case 'RefinementType': + case 'ReadonlyType': + return getProps(codec.type); + case 'InterfaceType': + case 'StrictType': + case 'PartialType': + return codec.props; + case 'IntersectionType': + const iTypes = codec.types as rt.HasProps[]; + return iTypes.reduce((props, type) => { + return Object.assign(props, getProps(type) as rt.Props); + }, {} as rt.Props) as rt.Props; + default: + return null; + } +}; + +const getExcessProps = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props: rt.Props | rt.RecordC, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + r: any +): string[] => { + return Object.keys(r).reduce((acc, k) => { + const codecChildren = get(props, [k]); + const childrenProps = getProps(codecChildren); + const childrenObject = r[k] as Record; + if (codecChildren != null && childrenProps != null && codecChildren._tag === 'DictionaryType') { + const keys = Object.keys(childrenObject); + return [ + ...acc, + ...keys.reduce( + (kAcc, i) => [...kAcc, ...getExcessProps(childrenProps, childrenObject[i])], + [] + ), + ]; + } + if (codecChildren != null && childrenProps != null) { + return [...acc, ...getExcessProps(childrenProps, childrenObject)]; + } else if (codecChildren == null) { + return [...acc, k]; + } + return acc; + }, []); +}; + +export const excess = < + C extends rt.InterfaceType | GenericIntersectionC | rt.PartialType +>( + codec: C +): C => { + const codecProps = getProps(codec); + + const r = new rt.InterfaceType( + codec.name, + codec.is, + (i, c) => + either.chain(rt.UnknownRecord.validate(i, c), (s) => { + if (codecProps == null) { + return rt.failure(i, c, 'unknown codec'); + } + const ex = getExcessProps(codecProps, s); + + return ex.length > 0 + ? rt.failure( + i, + c, + `Invalid value ${JSON.stringify(i)}, excess properties: ${JSON.stringify(ex)}` + ) + : codec.validate(i, c); + }), + codec.encode, + codecProps + ); + return r as C; +}; diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 8375296d869e6..dabdcf553edb4 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -20,7 +20,7 @@ import { loggingSystemMock, coreMock, } from '../../../../../../src/core/server/mocks'; -import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server'; +import * as kbnTestServer from '../../../../../../src/core/test_helpers/kbn_server'; import { SpacesService } from '../../spaces_service'; import { SpacesAuditLogger } from '../audit_logger'; import { convertSavedObjectToSpace } from '../../routes/lib'; diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index 1558c6425f542..998c6ca18983d 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -15,7 +15,7 @@ import { IRouter, } from '../../../../../../src/core/server'; -import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server'; +import * as kbnTestServer from '../../../../../../src/core/test_helpers/kbn_server'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; // FAILING: https://github.com/elastic/kibana/issues/58942 diff --git a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_copy_to_space_mocks.ts b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_copy_to_space_mocks.ts index 0e117b3f16e3f..ef6f5e1541a46 100644 --- a/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_copy_to_space_mocks.ts +++ b/x-pack/plugins/spaces/server/routes/api/__fixtures__/create_copy_to_space_mocks.ts @@ -5,7 +5,7 @@ */ import { Readable } from 'stream'; -import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils'; +import { createPromiseFromStreams, createConcatStream } from 'src/core/server/utils'; async function readStreamToCompletion(stream: Readable) { return (await (createPromiseFromStreams([stream, createConcatStream([])]) as unknown)) as any[]; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 5280f094e3a5d..2435d8a9aaf04 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -30,6 +30,9 @@ "properties": { "cannot_connect": { "type": "long" + }, + "not_found": { + "type": "long" } } }, @@ -64,6 +67,9 @@ "properties": { "cannot_connect": { "type": "long" + }, + "not_found": { + "type": "long" } } }, diff --git a/x-pack/plugins/transform/server/routes/api/field_histograms.ts b/x-pack/plugins/transform/server/routes/api/field_histograms.ts index f2fd81368ec17..2642040c4cd0d 100644 --- a/x-pack/plugins/transform/server/routes/api/field_histograms.ts +++ b/x-pack/plugins/transform/server/routes/api/field_histograms.ts @@ -34,7 +34,7 @@ export function registerFieldHistogramsRoutes({ router, license }: RouteDependen try { const resp = await getHistogramsForFields( - ctx.transform!.dataClient, + ctx.core.elasticsearch.client, indexPatternTitle, query, fields, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index df78975d21b07..eacb1febd20ff 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -501,47 +501,6 @@ "core.ui.securityNavList.label": "セキュリティ", "core.ui.welcomeErrorMessage": "Elasticが正常に読み込まれませんでした。詳細はサーバーアウトプットを確認してください。", "core.ui.welcomeMessage": "Elasticの読み込み中", - "core.ui_settings.params.darkModeText": "Kibana UI のダークモードを有効にします。この設定を適用するにはページの更新が必要です。", - "core.ui_settings.params.darkModeTitle": "ダークモード", - "core.ui_settings.params.dateFormat.dayOfWeekText": "週の初めの曜日を設定します", - "core.ui_settings.params.dateFormat.dayOfWeekTitle": "曜日", - "core.ui_settings.params.dateFormat.optionsLinkText": "フォーマット", - "core.ui_settings.params.dateFormat.scaled.intervalsLinkText": "ISO8601 間隔", - "core.ui_settings.params.dateFormat.scaledText": "時間ベースのデータが順番にレンダリングされ、フォーマットされたタイムスタンプが測定値の間隔に適応すべき状況で使用されるフォーマットを定義する値です。キーは {intervalsLink}。", - "core.ui_settings.params.dateFormat.scaledTitle": "スケーリングされたデータフォーマットです", - "core.ui_settings.params.dateFormat.timezoneText": "使用されるタイムゾーンです。{defaultOption} ではご使用のブラウザにより検知されたタイムゾーンが使用されます。", - "core.ui_settings.params.dateFormat.timezoneTitle": "データフォーマットのタイムゾーン", - "core.ui_settings.params.dateFormatText": "きちんとフォーマットされたデータを表示する際、この {formatLink} を使用します", - "core.ui_settings.params.dateFormatTitle": "データフォーマット", - "core.ui_settings.params.dateNanosFormatText": "Elasticsearch の {dateNanosLink} データタイプに使用されます", - "core.ui_settings.params.dateNanosFormatTitle": "ナノ秒フォーマットでの日付", - "core.ui_settings.params.dateNanosLinkTitle": "date_nanos", - "core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage": "相対 URL でなければなりません。", - "core.ui_settings.params.defaultRoute.defaultRouteText": "この設定は、Kibana 起動時のデフォルトのルートを設定します。この設定で、Kibana 起動時のランディングページを変更できます。経路は相対 URL でなければなりません。", - "core.ui_settings.params.defaultRoute.defaultRouteTitle": "デフォルトのルート", - "core.ui_settings.params.disableAnimationsText": "Kibana UI の不要なアニメーションをオフにします。変更を適用するにはページを更新してください。", - "core.ui_settings.params.disableAnimationsTitle": "アニメーションを無効にする", - "core.ui_settings.params.maxCellHeightText": "表のセルが使用する高さの上限です。この切り捨てを無効にするには 0 に設定します", - "core.ui_settings.params.maxCellHeightTitle": "表のセルの高さの上限", - "core.ui_settings.params.notifications.banner.markdownLinkText": "マークダウン対応", - "core.ui_settings.params.notifications.bannerLifetimeText": "バナー通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定するとカウントダウンが無効になります。", - "core.ui_settings.params.notifications.bannerLifetimeTitle": "バナー通知時間", - "core.ui_settings.params.notifications.bannerText": "すべてのユーザーへの一時的な通知を目的としたカスタムバナーです。{markdownLink}", - "core.ui_settings.params.notifications.bannerTitle": "カスタムバナー通知", - "core.ui_settings.params.notifications.errorLifetimeText": "エラー通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", - "core.ui_settings.params.notifications.errorLifetimeTitle": "エラー通知時間", - "core.ui_settings.params.notifications.infoLifetimeText": "情報通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", - "core.ui_settings.params.notifications.infoLifetimeTitle": "情報通知時間", - "core.ui_settings.params.notifications.warningLifetimeText": "警告通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定すると無効になります。", - "core.ui_settings.params.notifications.warningLifetimeTitle": "警告通知時間", - "core.ui_settings.params.pageNavigationDesc": "ナビゲーションのスタイルを変更", - "core.ui_settings.params.pageNavigationLegacy": "レガシー", - "core.ui_settings.params.pageNavigationModern": "モダン", - "core.ui_settings.params.pageNavigationName": "サイドナビゲーションスタイル", - "core.ui_settings.params.themeVersionText": "現在のバージョンと次のバージョンのKibanaで使用されるテーマを切り替えます。この設定を適用するにはページの更新が必要です。", - "core.ui_settings.params.themeVersionTitle": "テーマバージョン", - "core.ui_settings.params.storeUrlText": "URL は長くなりすぎてブラウザが対応できない場合があります。セッションストレージに URL の一部を保存することがで この問題に対処できるかテストしています。結果を教えてください!", - "core.ui_settings.params.storeUrlTitle": "セッションストレージに URL を格納", "dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化", "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全画面", "dashboard.addExistingVisualizationLinkText": "既存のユーザーを追加", @@ -714,8 +673,6 @@ "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "時間フィルターのデフォルト更新間隔「値」はミリ秒で指定する必要があります。", "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "タイムピッカーの更新間隔", "data.advancedSettings.timepicker.thisWeek": "今週", - "data.advancedSettings.timepicker.timeDefaultsText": "時間フィルターが選択されずに Kibana が起動した際に使用される時間フィルターです", - "data.advancedSettings.timepicker.timeDefaultsTitle": "デフォルトのタイムピッカー", "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", "data.common.kql.errors.endOfInputText": "インプットの終わり", @@ -1646,7 +1603,6 @@ "home.loadTutorials.unableToLoadErrorMessage": "チュートリアルが読み込めません。", "home.pageTitle": "ホーム", "home.recentlyAccessed.recentlyViewedTitle": "最近閲覧", - "home.sampleData.ecommerceSpec.averageSalesPerRegionTitle": "[e コマース] 地域ごとの平均売上", "home.sampleData.ecommerceSpec.averageSalesPriceTitle": "[e コマース] 平均販売価格", "home.sampleData.ecommerceSpec.averageSoldQuantityTitle": "[e コマース] 平均販売数", "home.sampleData.ecommerceSpec.controlsTitle": "[e コマース] コントロール", @@ -1677,7 +1633,6 @@ "home.sampleData.flightsSpec.globalFlightDashboardDescription": "ES-Air、Logstash Airways、Kibana Airlines、JetBeats のサンプル飛行データを分析します", "home.sampleData.flightsSpec.globalFlightDashboardTitle": "[フライト] グローバルフライトダッシュボード", "home.sampleData.flightsSpec.markdownInstructionsTitle": "[フライト] マークダウンの指示", - "home.sampleData.flightsSpec.originCountryTicketPricesTitle": "[フライト] 出発国の運賃", "home.sampleData.flightsSpec.originCountryTitle": "[Flights] 出発国と到着国の比較", "home.sampleData.flightsSpec.totalFlightCancellationsTitle": "[フライト] フライト欠航合計", "home.sampleData.flightsSpec.totalFlightDelaysTitle": "[フライト] フライト遅延合計", @@ -1692,7 +1647,6 @@ "home.sampleData.logsSpec.markdownInstructionsTitle": "[ログ] マークダウンの指示", "home.sampleData.logsSpec.responseCodesOverTimeTitle": "[ログ] 一定期間の応答コードと注釈", "home.sampleData.logsSpec.sourceAndDestinationSankeyChartTitle": "[ログ] ソースと行先のサンキーダイアグラム", - "home.sampleData.logsSpec.uniqueVisitorsByCountryTitle": "[ログ] 国ごとのユニークビジター", "home.sampleData.logsSpec.uniqueVisitorsTitle": "[ログ] ユニークビジターと平均バイトの比較", "home.sampleData.logsSpec.visitorOSTitle": "[ログ] OS 別のビジター", "home.sampleData.logsSpec.webTrafficDescription": "Elastic Web サイトのサンプル Webトラフィックログデータを分析します", @@ -4433,8 +4387,6 @@ "visualizations.newVisWizard.searchSelection.savedObjectType.search": "保存検索", "visualizations.newVisWizard.selectVisType": "ビジュアライゼーションのタイプを選択してください", "visualizations.newVisWizard.title": "新規ビジュアライゼーション", - "visualizations.newVisWizard.visTypeAliasDescription": "Visualize 外で Kibana アプリケーションを開きます。", - "visualizations.newVisWizard.visTypeAliasTitle": "Kibana アプリケーション", "visualizations.savedObjectName": "ビジュアライゼーション", "visualizations.visualizationTypeInvalidMessage": "無効なビジュアライゼーションタイプ \"{visType}\"", "visualize.badge.readOnly.text": "読み取り専用", @@ -4533,7 +4485,6 @@ "xpack.actions.serverSideErrors.predefinedActionUpdateDisabled": "あらかじめ構成されたアクション{id}は更新できません。", "xpack.actions.serverSideErrors.unavailableLicenseErrorMessage": "現時点でライセンス情報を入手できないため、アクションタイプ {actionTypeId} は無効です。", "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "グラフを利用できません。現在ライセンス情報が利用できません。", - "xpack.actions.urlAllowedHostsConfigurationError": "target {field} \"{value}\" は Kibana 構成 xpack.actions.allowedHosts にはホワイトリスト化されていません。", "xpack.alertingBuiltins.indexThreshold.actionGroupThresholdMetTitle": "しきい値一致", "xpack.alertingBuiltins.indexThreshold.actionVariableContextDateLabel": "アラートがしきい値を超えた日付。", "xpack.alertingBuiltins.indexThreshold.actionVariableContextGroupLabel": "しきい値を超えたグループ。", @@ -4679,6 +4630,11 @@ "xpack.apm.agentMetrics.java.threadCountMax": "最高カウント", "xpack.apm.alertTypes.errorRate": "エラー率", "xpack.apm.alertTypes.transactionDuration": "トランザクション期間", + "xpack.apm.anomaly_detection.error.invalid_license": "異常検知を使用するには、Elastic Platinumライセンスのサブスクリプションが必要です。このライセンスがあれば、機械学習を活用して、サービスを監視できます。", + "xpack.apm.anomaly_detection.error.missing_read_privileges": "異常検知ジョブを表示するには、機械学習およびAPMの「読み取り」権限が必要です", + "xpack.apm.anomaly_detection.error.missing_write_privileges": "異常検知ジョブを作成するには、機械学習およびAPMの「書き込み」権限が必要です", + "xpack.apm.anomaly_detection.error.not_available": "機械学習を利用できません", + "xpack.apm.anomaly_detection.error.not_available_in_space": "選択したスペースでは、機械学習を利用できません", "xpack.apm.anomalyDetection.createJobs.failed.text": "APMサービス環境用に[{environments}]1つ以上の異常検知ジョブを作成しているときに問題が発生しました。エラー: 「{errorMessage}」", "xpack.apm.anomalyDetection.createJobs.failed.title": "異常検知ジョブを作成できませんでした", "xpack.apm.anomalyDetection.createJobs.succeeded.text": "APMサービス環境[{environments}]の異常検知ジョブが正常に作成されました。機械学習がトラフィック異常値の分析を開始するには、少し時間がかかります。", @@ -4726,7 +4682,7 @@ "xpack.apm.errorGroupDetails.logMessageLabel": "ログメッセージ", "xpack.apm.errorGroupDetails.noErrorsLabel": "エラーが見つかりませんでした", "xpack.apm.errorGroupDetails.occurrencesChartLabel": "オカレンス", - "xpack.apm.errorGroupDetails.occurrencesLongLabel": "{occCount} 件", + "xpack.apm.errorGroupDetails.occurrencesLongLabel": "{occCount} {occCount, plural, one {件の発生} other {件の発生}}", "xpack.apm.errorGroupDetails.occurrencesShortLabel": "{occCount} 件", "xpack.apm.errorGroupDetails.relatedTransactionSample": "関連トランザクションサンプル", "xpack.apm.errorGroupDetails.unhandledLabel": "未対応", @@ -4753,7 +4709,6 @@ "xpack.apm.fetcher.error.title": "リソースの取得中にエラーが発生しました", "xpack.apm.fetcher.error.url": "URL", "xpack.apm.filter.environment.label": "環境", - "xpack.apm.filter.environment.allLabel": "すべて", "xpack.apm.filter.environment.notDefinedLabel": "未定義", "xpack.apm.filter.environment.selectEnvironmentLabel": "環境を選択", "xpack.apm.formatters.hoursTimeUnitLabel": "h", @@ -4832,7 +4787,6 @@ "xpack.apm.metrics.transactionChart.transactionDurationLabel": "トランザクション時間", "xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel": "1 分あたりのトランザクション数", "xpack.apm.notAvailableLabel": "N/A", - "xpack.apm.percentOfParent": "({value} {parentType, select, transaction {トランザクション} trace {トレース} })", "xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "利用可能なデータがありません", "xpack.apm.propertiesTable.agentFeature.noResultFound": "\"{value}\"に対する結果が見つかりませんでした。", "xpack.apm.propertiesTable.tabs.exceptionStacktraceLabel": "例外のスタックトレース", @@ -4875,7 +4829,10 @@ "xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric": "スコア(最大)", "xpack.apm.serviceMap.anomalyDetectionPopoverTitle": "異常検知", "xpack.apm.serviceMap.anomalyDetectionPopoverTooltip": "サービス正常性インジケーターは、機械学習の異常検知に基づいています。", + "xpack.apm.serviceMap.avgCpuUsagePopoverStat": "CPU使用状況(平均)", + "xpack.apm.serviceMap.avgMemoryUsagePopoverStat": "メモリー使用状況(平均)", "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "1分あたりのリクエスト(平均)", + "xpack.apm.serviceMap.avgTransDurationPopoverStat": "トランザクションの長さ(平均)", "xpack.apm.serviceMap.betaBadge": "ベータ", "xpack.apm.serviceMap.betaTooltipMessage": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。", "xpack.apm.serviceMap.center": "中央", @@ -4883,10 +4840,13 @@ "xpack.apm.serviceMap.emptyBanner.docsLink": "詳細はドキュメントをご覧ください", "xpack.apm.serviceMap.emptyBanner.message": "接続されているサービスや外部リクエストを検出できる場合、システムはそれらをマップします。最新版の APM エージェントが動作していることを確認してください。", "xpack.apm.serviceMap.emptyBanner.title": "単一のサービスしかないようです。", + "xpack.apm.serviceMap.errorRatePopoverStat": "トランザクションエラー率(平均)", "xpack.apm.serviceMap.focusMapButtonText": "焦点マップ", "xpack.apm.serviceMap.invalidLicenseMessage": "サービスマップを利用するには、Elastic Platinum ライセンスが必要です。これにより、APM データとともにアプリケーションスタック全てを可視化することができるようになります。", "xpack.apm.serviceMap.popoverMetrics.noDataText": "選択した環境のデータがありません。別の環境に切り替えてください。", "xpack.apm.serviceMap.serviceDetailsButtonText": "サービス詳細", + "xpack.apm.serviceMap.subtypePopoverStat": "サブタイプ", + "xpack.apm.serviceMap.typePopoverStat": "タイプ", "xpack.apm.serviceMap.viewFullMap": "サービスの全体マップを表示", "xpack.apm.serviceMap.zoomIn": "ズームイン", "xpack.apm.serviceMap.zoomOut": "ズームアウト", @@ -4932,7 +4892,7 @@ "xpack.apm.settings.anomalyDetection.jobList.emptyListText": "異常検知ジョブがありません。", "xpack.apm.settings.anomalyDetection.jobList.environmentColumnLabel": "環境", "xpack.apm.settings.anomalyDetection.jobList.environments": "環境", - "xpack.apm.settings.anomalyDetection.jobList.failedFetchText": "異常検知ジョブを取得できませんでした。", + "xpack.apm.settings.anomalyDetection.jobList.failedFetchText": "異常検知ジョブを取得できません。", "xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText": "異常検知を新しい環境に追加するには、機械学習ジョブを作成します。既存の機械学習ジョブは、{mlJobsLink}で管理できます。", "xpack.apm.settings.anomalyDetection.jobList.mlDescriptionText.mlJobsLinkText": "機械学習", "xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText": "MLでジョブを表示", @@ -5406,7 +5366,7 @@ "xpack.canvas.elementSettings.dataTabLabel": "データ", "xpack.canvas.elementSettings.displayTabLabel": "表示", "xpack.canvas.embedObject.noMatchingObjectsMessage": "一致するオブジェクトが見つかりませんでした。", - "xpack.canvas.embedObject.titleText": "Visualizeライブラリから追加", + "xpack.canvas.embedObject.titleText": "Kibanaから追加", "xpack.canvas.error.actionsElements.invaludArgIndexErrorMessage": "無効な引数インデックス: {index}", "xpack.canvas.error.downloadWorkpad.downloadFailureErrorMessage": "ワークパッドをダウンロードできませんでした", "xpack.canvas.error.downloadWorkpad.downloadRenderedWorkpadFailureErrorMessage": "レンダリングされたワークパッドをダウンロードできませんでした", @@ -6293,7 +6253,7 @@ "xpack.canvas.workpadHeaderElementMenu.chartMenuItemLabel": "グラフ", "xpack.canvas.workpadHeaderElementMenu.elementMenuButtonLabel": "エレメントを追加", "xpack.canvas.workpadHeaderElementMenu.elementMenuLabel": "要素を追加", - "xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel": "Visualizeライブラリから追加", + "xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel": "Kibanaから追加", "xpack.canvas.workpadHeaderElementMenu.filterMenuItemLabel": "フィルター", "xpack.canvas.workpadHeaderElementMenu.imageMenuItemLabel": "画像", "xpack.canvas.workpadHeaderElementMenu.manageAssetsMenuItemLabel": "アセットの管理", @@ -6746,6 +6706,9 @@ "xpack.enterpriseSearch.errorConnectingState.description4": "セットアップガイドを確認するか、サーバーログの{pluginLog}ログメッセージを確認してください。", "xpack.enterpriseSearch.errorConnectingState.setupGuideCta": "セットアップガイドを確認", "xpack.enterpriseSearch.errorConnectingState.title": "接続できません", + "xpack.enterpriseSearch.errorConnectingState.troubleshootAuth": "ユーザー認証を確認してください。", + "xpack.enterpriseSearch.errorConnectingState.troubleshootAuthNative": "Elasticsearchネイティブ認証またはSSO/SAMLを使用して認証する必要があります。", + "xpack.enterpriseSearch.errorConnectingState.troubleshootAuthSAML": "SSO/SAMLを使用している場合は、エンタープライズ サーチでSAMLレルムも設定する必要があります。", "xpack.enterpriseSearch.setupGuide.step1.instruction1": "{configFile}ファイルで、{configSetting}を{productName}インスタンスのURLに設定します。例:", "xpack.enterpriseSearch.setupGuide.step1.title": "{productName}ホストURLをKibana構成に追加", "xpack.enterpriseSearch.setupGuide.step2.instruction1": "Kibanaを再起動して、前のステップから構成変更を取得します。", @@ -8100,7 +8063,6 @@ "xpack.indexLifecycleMgmt.editPolicy.learnAboutShardAllocationLink": "シャード割り当ての詳細をご覧ください", "xpack.indexLifecycleMgmt.editPolicy.learnAboutTimingText": "タイミングの詳細をご覧ください", "xpack.indexLifecycleMgmt.editPolicy.lifecyclePolicyDescriptionText": "インデックスへのアクティブな書き込みから削除までの、インデックスライフサイクルの 4 つのフェーズを自動化するには、インデックスポリシーを使用します。", - "xpack.indexLifecycleMgmt.editPolicy.loadPolicyErrorMessage": "ポリシーの読み込み中にエラーが発生しました", "xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError": "最高年齢が必要です。", "xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError": "最高ドキュメント数が必要です。", "xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError": "最大インデックスサイズが必要です。", @@ -8239,7 +8201,6 @@ "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title": "インデックステンプレートにポリシー「{name}」 を追加", "xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText": "インデックステンプレートにポリシーを追加", "xpack.indexLifecycleMgmt.policyTable.captionText": "以下は {total} 列中 {count, plural, one {# 列} other {# 列}} を含むインデックスライフサイクルポリシー表です。", - "xpack.indexLifecycleMgmt.policyTable.deletedPoliciesText": "{numSelected} 件の{numSelected, plural, one {ポリシー} other {ポリシー}}が削除されました", "xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip": "インデックスが使用中のポリシーは削除できません", "xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText": "ポリシーを削除", "xpack.indexLifecycleMgmt.policyTable.emptyPrompt.createButtonLabel": "ポリシーを作成", @@ -8464,6 +8425,7 @@ "xpack.infra.logs.analysis.anomalySectionLogRateChartNoData": "表示するログレートデータがありません。", "xpack.infra.logs.analysis.anomalySectionNoDataBody": "時間範囲を調整する必要があるかもしれません。", "xpack.infra.logs.analysis.anomalySectionNoDataTitle": "表示するデータがありません。", + "xpack.infra.logs.analysis.createJobButtonLabel": "MLジョブを作成", "xpack.infra.logs.analysis.datasetFilterPlaceholder": "データセットでフィルター", "xpack.infra.logs.analysis.enableAnomalyDetectionButtonLabel": "異常検知を有効にする", "xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutMessage": "異なるソース構成を使用して{moduleName} MLジョブが作成されました。現在の構成を適用するにはジョブを再作成してください。これにより以前検出された異常が削除されます。", @@ -8479,6 +8441,9 @@ "xpack.infra.logs.analysis.logEntryRateModuleDescription": "機械学習を使用して自動的に異常ログエントリ率を検出します。", "xpack.infra.logs.analysis.logEntryRateModuleName": "ログレート", "xpack.infra.logs.analysis.manageMlJobsButtonLabel": "MLジョブの管理", + "xpack.infra.logs.analysis.missingMlPrivilegesTitle": "追加の機械学習の権限が必要です", + "xpack.infra.logs.analysis.missingMlResultsPrivilegesDescription": "本機能は機械学習ジョブを利用し、そのステータスと結果にアクセスするためには、少なくとも機械学習アプリの読み取り権限が必要です。", + "xpack.infra.logs.analysis.missingMlSetupPrivilegesDescription": "本機能は機械学習ジョブを利用し、設定には機械学習アプリのすべての権限が必要です。", "xpack.infra.logs.analysis.mlAppButton": "機械学習を開く", "xpack.infra.logs.analysis.mlUnavailableBody": "詳細は{machineLearningAppLink}をご覧ください。", "xpack.infra.logs.analysis.mlUnavailableTitle": "この機能には機械学習が必要です", @@ -8499,7 +8464,6 @@ "xpack.infra.logs.customizeLogs.customizeButtonLabel": "カスタマイズ", "xpack.infra.logs.customizeLogs.lineWrappingFormRowLabel": "改行", "xpack.infra.logs.customizeLogs.textSizeFormRowLabel": "テキストサイズ", - "xpack.infra.logs.customizeLogs.textSizeRadioGroup": "{textScale, select, small {小さい} medium {中くらい} large {大きい} other {{textScale}}}", "xpack.infra.logs.customizeLogs.wrapLongLinesSwitchLabel": "長い行を改行", "xpack.infra.logs.emptyView.checkForNewDataButtonLabel": "新規データを確認", "xpack.infra.logs.emptyView.noLogMessageDescription": "フィルターを調整してみてください。", @@ -8529,7 +8493,7 @@ "xpack.infra.logs.logAnalysis.splash.loadingMessage": "ライセンスを確認しています...", "xpack.infra.logs.logAnalysis.splash.splashImageAlt": "プレースホルダー画像", "xpack.infra.logs.logAnalysis.splash.startTrialCta": "トライアルを開始", - "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "14日間無料の試用版には、機械学習機能が含まれており、ログで異常を検出することができます。", + "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "無料の試用版には、機械学習機能が含まれており、ログで異常を検出することができます。", "xpack.infra.logs.logAnalysis.splash.startTrialTitle": "異常検知を利用するには、無料の試用版を開始してください", "xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "サブスクリプションのアップグレード", "xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "機械学習機能を使用するには、プラチナサブスクリプションが必要です。", @@ -8728,11 +8692,11 @@ "xpack.infra.metrics.alertFlyout.alertName": "メトリックしきい値", "xpack.infra.metrics.alertFlyout.alertOnNoData": "データがない場合に通知する", "xpack.infra.metrics.alertFlyout.alertPreviewError": "このアラート条件をプレビューするときにエラーが発生しました", + "xpack.infra.metrics.alertFlyout.alertPreviewErrorDesc": "しばらくたってから再試行するか、詳細を確認してください。", "xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "一部のデータを評価するときにエラーが発生しました。", - "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} {groupName}", "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "すべてを対象にする", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "データがない {boldedResultsNumber}結果がありました。", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber": "{noData}", + "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "データなしの件数:{boldedResultsNumber}", + "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber": "{noData, plural, one {件の結果がありました} other {件の結果がありました}}", "xpack.infra.metrics.alertFlyout.alertPreviewResult": "このアラートは{firedTimes}回発生しました", "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "過去{lookback}", "xpack.infra.metrics.alertFlyout.conditions": "条件", @@ -8744,6 +8708,7 @@ "xpack.infra.metrics.alertFlyout.error.thresholdRequired": "しきい値が必要です。", "xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired": "しきい値には有効な数値を含める必要があります。", "xpack.infra.metrics.alertFlyout.error.timeRequred": "ページサイズが必要です。", + "xpack.infra.metrics.alertFlyout.errorDetails": "詳細", "xpack.infra.metrics.alertFlyout.expandRowLabel": "行を展開します。", "xpack.infra.metrics.alertFlyout.expression.for.descriptionLabel": "対象", "xpack.infra.metrics.alertFlyout.expression.for.popoverTitle": "インベントリタイプ", @@ -8770,8 +8735,12 @@ "xpack.infra.metrics.alertFlyout.tooManyBucketsErrorDescription": "選択するプレビュー長を短くするか、{forTheLast}フィールドの時間を増やしてください。", "xpack.infra.metrics.alertFlyout.tooManyBucketsErrorTitle": "データが多すぎます(>{maxBuckets}結果)", "xpack.infra.metrics.alertFlyout.weekLabel": "週", + "xpack.infra.metrics.alerting.alertStateActionVariableDescription": "現在のアラートの状態", + "xpack.infra.metrics.alerting.groupActionVariableDescription": "データを報告するグループの名前", "xpack.infra.metrics.alerting.inventory.threshold.defaultActionMessage": "\\{\\{alertName\\}\\} - \\{\\{context.group\\}\\}は状態\\{\\{context.alertState\\}\\}です\n\n理由:\n\\{\\{context.reason\\}\\}\n", "xpack.infra.metrics.alerting.inventory.threshold.fired": "実行", + "xpack.infra.metrics.alerting.metricActionVariableDescription": "指定された条件のメトリック名。使用方法:(ctx.metric.condition0、ctx.metric.condition1など)。", + "xpack.infra.metrics.alerting.reasonActionVariableDescription": "どのメトリックがどのしきい値を超えたのかを含む、アラートがこの状態である理由に関する説明", "xpack.infra.metrics.alerting.threshold.aboveRecovery": "より大", "xpack.infra.metrics.alerting.threshold.alertState": "アラート", "xpack.infra.metrics.alerting.threshold.belowRecovery": "より小", @@ -8792,6 +8761,9 @@ "xpack.infra.metrics.alerting.threshold.outsideRangeComparator": "の間にない", "xpack.infra.metrics.alerting.threshold.recoveredAlertReason": "{metric}は{comparator} {threshold}のしきい値です(現在の値は{currentValue})", "xpack.infra.metrics.alerting.threshold.thresholdRange": "{a}と{b}", + "xpack.infra.metrics.alerting.thresholdActionVariableDescription": "指定された条件のメトリックのしきい値。使用方法:(ctx.threshold.condition0、ctx.threshold.condition1など)。", + "xpack.infra.metrics.alerting.timestampDescription": "アラートが検出された時点のタイムスタンプ。", + "xpack.infra.metrics.alerting.valueActionVariableDescription": "指定された条件のメトリックの値。使用方法:(ctx.value.condition0、ctx.value.condition1など)。", "xpack.infra.metrics.alerts.dataTimeRangeLabel": "過去{lookback} {timeLabel}", "xpack.infra.metrics.alerts.dataTimeRangeLabelWithGrouping": "{id}のデータの過去{lookback} {timeLabel}", "xpack.infra.metrics.alerts.loadingMessage": "読み込み中", @@ -8810,7 +8782,6 @@ "xpack.infra.metrics.missingTSVBModelError": "{nodeType}では{metricId}のTSVBモデルが存在しません", "xpack.infra.metrics.pluginTitle": "メトリック", "xpack.infra.metrics.refetchButtonLabel": "新規データを確認", - "xpack.infra.metricsExplorer.everything": "すべて", "xpack.infra.metricsExplorer.actionsLabel.aria": "{grouping} のアクション", "xpack.infra.metricsExplorer.actionsLabel.button": "アクション", "xpack.infra.metricsExplorer.aggregationLabel": "/", @@ -8902,7 +8873,7 @@ "xpack.infra.savedView.manageViews": "ビューの管理", "xpack.infra.savedView.saveNewView": "新しいビューの保存", "xpack.infra.savedView.searchPlaceholder": "保存されたビューの検索", - "xpack.infra.savedView.unknownView": "不明", + "xpack.infra.savedView.unknownView": "ビューが選択されていません", "xpack.infra.savedView.updateView": "ビューの更新", "xpack.infra.snapshot.missingSnapshotMetricError": "{nodeType}の{metric}の集約を使用できません。", "xpack.infra.sourceConfiguration.addLogColumnButtonLabel": "列を追加", @@ -9053,54 +9024,11 @@ "xpack.infra.waffle.unableToSelectMetricErrorTitle": "メトリックのオプションまたは値を選択できません", "xpack.infra.waffleTime.autoRefreshButtonLabel": "自動更新", "xpack.infra.waffleTime.stopRefreshingButtonLabel": "更新中止", - "xpack.ingestManager.agentPolicy.confirmModalCalloutDescription": "選択されたエージェント構成{policyName}が一部のエージェントで既に使用されていることをフリートが検出しました。このアクションの結果として、フリートはこの構成で使用されているすべてのエージェントに更新をデプロイします。", - "xpack.ingestManager.agentPolicy.confirmModalCancelButtonLabel": "キャンセル", - "xpack.ingestManager.agentPolicy.confirmModalConfirmButtonLabel": "変更を保存してデプロイ", - "xpack.ingestManager.agentPolicy.confirmModalDescription": "このアクションは元に戻せません。続行していいですか?", - "xpack.ingestManager.agentPolicy.confirmModalTitle": "変更を保存してデプロイ", - "xpack.ingestManager.agentPolicy.linkedAgentCountText": "{count, plural, one {# エージェント} other {# エージェント}}", - "xpack.ingestManager.agentPolicyActionMenu.buttonText": "アクション", - "xpack.ingestManager.agentPolicyActionMenu.copyPolicyActionText": "構成のコピー", - "xpack.ingestManager.agentPolicyActionMenu.enrollAgentActionText": "エージェントの追加", - "xpack.ingestManager.agentPolicyActionMenu.viewPolicyText": "構成を表示", - "xpack.ingestManager.agentPolicyForm.advancedOptionsToggleLabel": "高度なオプション", - "xpack.ingestManager.agentPolicyForm.descriptionFieldLabel": "説明", - "xpack.ingestManager.agentPolicyForm.descriptionFieldPlaceholder": "この構成がどのように使用されるか", - "xpack.ingestManager.agentPolicyForm.monitoringDescription": "パフォーマンスのデバッグと追跡のために、エージェントに関するデータを収集します。", - "xpack.ingestManager.agentPolicyForm.monitoringLabel": "アラート監視", - "xpack.ingestManager.agentPolicyForm.monitoringLogsFieldLabel": "エージェントログを収集", - "xpack.ingestManager.agentPolicyForm.monitoringMetricsFieldLabel": "エージェントメトリックを収集", - "xpack.ingestManager.agentPolicyForm.nameFieldLabel": "名前", - "xpack.ingestManager.agentPolicyForm.nameFieldPlaceholder": "名前を選択", - "xpack.ingestManager.agentPolicyForm.nameRequiredErrorMessage": "エージェント構成名が必要です", - "xpack.ingestManager.agentPolicyForm.namespaceFieldDescription": "この構成を使用する統合にデフォルトの名前空間を適用します。統合はその独自の名前空間を指定できます。", - "xpack.ingestManager.agentPolicyForm.namespaceFieldLabel": "デフォルト名前空間", - "xpack.ingestManager.agentPolicyForm.namespaceRequiredErrorMessage": "ネームスペースが必要です", - "xpack.ingestManager.agentPolicyForm.systemMonitoringFieldLabel": "オプション", - "xpack.ingestManager.agentPolicyForm.systemMonitoringText": "システムメトリックを収集", - "xpack.ingestManager.agentPolicyForm.systemMonitoringTooltipText": "このオプションを有効にすると、システムメトリックと情報を収集する統合で構成をブートストラップできます。", - "xpack.ingestManager.agentPolicyList.actionsColumnTitle": "アクション", - "xpack.ingestManager.agentPolicyList.addButton": "エージェント構成を作成", - "xpack.ingestManager.agentPolicyList.agentsColumnTitle": "エージェント", - "xpack.ingestManager.agentPolicyList.clearFiltersLinkText": "フィルターを消去", - "xpack.ingestManager.agentPolicyList.descriptionColumnTitle": "説明", - "xpack.ingestManager.agentPolicyList.loadingAgentPoliciesMessage": "エージェント構成を読み込み中...", - "xpack.ingestManager.agentPolicyList.nameColumnTitle": "名前", - "xpack.ingestManager.agentPolicyList.noAgentPoliciesPrompt": "エージェント構成なし", - "xpack.ingestManager.agentPolicyList.noFilteredAgentPoliciesPrompt": "エージェント構成が見つかりません。 {clearFiltersLink}", - "xpack.ingestManager.agentPolicyList.packagePoliciesCountColumnTitle": "統合", - "xpack.ingestManager.agentPolicyList.pageSubtitle": "エージェント構成を使用すると、エージェントとエージェントが収集するデータを管理できます。", - "xpack.ingestManager.agentPolicyList.pageTitle": "エージェント構成", - "xpack.ingestManager.agentPolicyList.reloadAgentPoliciesButtonText": "再読み込み", - "xpack.ingestManager.agentPolicyList.revisionNumber": "rev. {revNumber}", - "xpack.ingestManager.agentPolicyList.updatedOnColumnTitle": "最終更新日:", "xpack.ingestManager.agentDetails.actionsButton": "アクション", - "xpack.ingestManager.agentDetails.agentPolicyLabel": "エージェント構成", "xpack.ingestManager.agentDetails.agentDetailsTitle": "エージェント'{id}'", "xpack.ingestManager.agentDetails.agentNotFoundErrorDescription": "エージェントID {agentId}が見つかりません", "xpack.ingestManager.agentDetails.agentNotFoundErrorTitle": "エージェントが見つかりません", - "xpack.ingestManager.agentDetails.policyLabel": "構成", - "xpack.ingestManager.agentDetails.hostIdLabel": "ホストID", + "xpack.ingestManager.agentDetails.hostIdLabel": "エージェントID", "xpack.ingestManager.agentDetails.hostNameLabel": "ホスト名", "xpack.ingestManager.agentDetails.localMetadataSectionSubtitle": "メタデータを読み込み中", "xpack.ingestManager.agentDetails.metadataSectionTitle": "メタデータ", @@ -9114,21 +9042,16 @@ "xpack.ingestManager.agentDetails.viewAgentListTitle": "すべてのエージェントを表示", "xpack.ingestManager.agentEnrollment.cancelButtonLabel": "キャンセル", "xpack.ingestManager.agentEnrollment.continueButtonLabel": "続行", - "xpack.ingestManager.agentEnrollment.copyPolicyButton": "クリップボードにコピー", "xpack.ingestManager.agentEnrollment.copyRunInstructionsButton": "クリップボードにコピー", - "xpack.ingestManager.agentEnrollment.downloadPolicyButton": "構成のダウンロード", "xpack.ingestManager.agentEnrollment.downloadDescription": "ホストのコンピューターでElasticエージェントをダウンロードします。Elasticエージェントダウンロードページでは、エージェントバイナリと検証署名にアクセスできます。", "xpack.ingestManager.agentEnrollment.downloadLink": "elastic.co/downloadsに移動", "xpack.ingestManager.agentEnrollment.enrollFleetTabLabel": "フリートに登録", "xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "スタンドアロンモード", "xpack.ingestManager.agentEnrollment.fleetNotInitializedText": "エージェントを登録する前に、フリートを設定する必要があります。{link}", "xpack.ingestManager.agentEnrollment.flyoutTitle": "エージェントの追加", - "xpack.ingestManager.agentEnrollment.goToFleetButton": "フリートに移動します。", "xpack.ingestManager.agentEnrollment.managedDescription": "必要なエージェントの数に関係なく、Fleetでは、簡単に一元的に更新を管理し、エージェントにデプロイすることができます。次の手順に従い、Elasticエージェントをダウンロードし、Fleetに登録してください。", "xpack.ingestManager.agentEnrollment.standaloneDescription": "スタンドアロンモードで実行中のエージェントは、構成を変更したい場合には、手動で更新する必要があります。次の手順に従い、スタンドアロンモードでElasticエージェントをダウンロードし、セットアップしてください。", - "xpack.ingestManager.agentEnrollment.stepCheckForDataDescription": "エージェントを起動した後、エージェントはデータの送信を開始します。Ingest Managerのデータセットページでは、このデータを確認できます。", "xpack.ingestManager.agentEnrollment.stepCheckForDataTitle": "データを確認", - "xpack.ingestManager.agentEnrollment.stepChooseAgentPolicyTitle": "エージェント構成を選択", "xpack.ingestManager.agentEnrollment.stepConfigureAgentDescription": "この構成をコピーし、Elasticエージェントがインストールされているシステムのファイル{fileName}に置きます。必ず、構成ファイルの{outputSection}セクションで{ESUsernameVariable}と{ESPasswordVariable}を修正し、実際のElasticsearch認証資格情報が使用されるようにしてください。", "xpack.ingestManager.agentEnrollment.stepConfigureAgentTitle": "エージェントの構成", "xpack.ingestManager.agentEnrollment.stepDownloadAgentTitle": "Elasticエージェントをダウンロード", @@ -9146,8 +9069,8 @@ "xpack.ingestManager.agentEventsList.timestampColumnTitle": "タイムスタンプ", "xpack.ingestManager.agentEventsList.typeColumnTitle": "タイプ", "xpack.ingestManager.agentEventSubtype.acknowledgedLabel": "認識", - "xpack.ingestManager.agentEventSubtype.policyLabel": "構成", "xpack.ingestManager.agentEventSubtype.dataDumpLabel": "データダンプ", + "xpack.ingestManager.agentEventSubtype.degradedLabel": "劣化", "xpack.ingestManager.agentEventSubtype.failedLabel": "失敗", "xpack.ingestManager.agentEventSubtype.inProgressLabel": "進行中", "xpack.ingestManager.agentEventSubtype.runningLabel": "実行中", @@ -9172,9 +9095,8 @@ "xpack.ingestManager.agentList.actionsColumnTitle": "アクション", "xpack.ingestManager.agentList.addButton": "エージェントの追加", "xpack.ingestManager.agentList.clearFiltersLinkText": "フィルターを消去", - "xpack.ingestManager.agentList.policyColumnTitle": "エージェント構成", - "xpack.ingestManager.agentList.policyFilterText": "エージェント構成", "xpack.ingestManager.agentList.enrollButton": "エージェントの追加", + "xpack.ingestManager.agentList.forceUnenrollOneButton": "強制的に登録解除する", "xpack.ingestManager.agentList.hostColumnTitle": "ホスト", "xpack.ingestManager.agentList.lastCheckinTitle": "前回のアクティビティ", "xpack.ingestManager.agentList.loadingAgentsMessage": "エージェントを読み込み中...", @@ -9196,13 +9118,6 @@ "xpack.ingestManager.agentListStatus.offlineLabel": "オフライン", "xpack.ingestManager.agentListStatus.onlineLabel": "オンライン", "xpack.ingestManager.agentListStatus.totalLabel": "エージェント", - "xpack.ingestManager.agentReassignPolicy.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.agentReassignPolicy.policyDescription": "選択したエージェント構成は、{count, plural, one {{countValue}個の統合} other {{countValue}個の統合}}のデータを収集します。", - "xpack.ingestManager.agentReassignPolicy.continueButtonLabel": "構成を割り当て", - "xpack.ingestManager.agentReassignPolicy.flyoutDescription": "選択したエージェントに割り当てる、新しいエージェント構成を選択します。", - "xpack.ingestManager.agentReassignPolicy.flyoutTitle": "新しいエージェント構成を割り当て", - "xpack.ingestManager.agentReassignPolicy.selectPolicyLabel": "エージェント構成", - "xpack.ingestManager.agentReassignPolicy.successSingleNotificationTitle": "新しいエージェント構成が再割り当てされました", "xpack.ingestManager.alphaMessageDescription": "Ingest Managerは本番環境用ではありません。", "xpack.ingestManager.alphaMessageLinkText": "詳細を参照してください。", "xpack.ingestManager.alphaMessageTitle": "ベータリリース", @@ -9212,7 +9127,6 @@ "xpack.ingestManager.alphaMessaging.forumLink": "ディスカッションフォーラム", "xpack.ingestManager.alphaMessaging.introText": "Ingest Managerは開発中であり、本番環境用ではありません。このベータリリースは、ユーザーがIngest Managerと新しいElasticエージェントをテストしてフィードバックを提供することを目的としています。このプラグインには、サポートSLAが適用されません。", "xpack.ingestManager.alphaMessging.closeFlyoutLabel": "閉じる", - "xpack.ingestManager.appNavigation.policiesLinkText": "構成", "xpack.ingestManager.appNavigation.dataStreamsLinkText": "データセット", "xpack.ingestManager.appNavigation.epmLinkText": "統合", "xpack.ingestManager.appNavigation.fleetLinkText": "フリート", @@ -9222,102 +9136,15 @@ "xpack.ingestManager.appTitle": "Ingest Manager", "xpack.ingestManager.betaBadge.labelText": "ベータ", "xpack.ingestManager.betaBadge.tooltipText": "このプラグインは本番環境用ではありません。バグについてはディスカッションフォーラムで報告してください。", - "xpack.ingestManager.breadcrumbs.addPackagePolicyPageTitle": "統合の追加", "xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle": "すべて", "xpack.ingestManager.breadcrumbs.appTitle": "Ingest Manager", - "xpack.ingestManager.breadcrumbs.policiesPageTitle": "構成", "xpack.ingestManager.breadcrumbs.datastreamsPageTitle": "データセット", - "xpack.ingestManager.breadcrumbs.editPackagePolicyPageTitle": "統合の編集", "xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle": "エージェント", "xpack.ingestManager.breadcrumbs.fleetEnrollmentTokensPageTitle": "登録トークン", "xpack.ingestManager.breadcrumbs.fleetPageTitle": "フリート", "xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle": "インストール済み", "xpack.ingestManager.breadcrumbs.integrationsPageTitle": "統合", "xpack.ingestManager.breadcrumbs.overviewPageTitle": "概要", - "xpack.ingestManager.policyDetails.addPackagePolicyButtonText": "統合の追加", - "xpack.ingestManager.policyDetails.policyDetailsTitle": "構成「{id}」", - "xpack.ingestManager.policyDetails.policyNotFoundErrorTitle": "構成「{id}」が見つかりません", - "xpack.ingestManager.policyDetails.packagePoliciesTable.actionsColumnTitle": "アクション", - "xpack.ingestManager.policyDetails.packagePoliciesTable.deleteActionTitle": "統合の削除", - "xpack.ingestManager.policyDetails.packagePoliciesTable.descriptionColumnTitle": "説明", - "xpack.ingestManager.policyDetails.packagePoliciesTable.editActionTitle": "統合の編集", - "xpack.ingestManager.policyDetails.packagePoliciesTable.nameColumnTitle": "名前", - "xpack.ingestManager.policyDetails.packagePoliciesTable.namespaceColumnTitle": "名前空間", - "xpack.ingestManager.policyDetails.packagePoliciesTable.packageNameColumnTitle": "統合", - "xpack.ingestManager.policyDetails.subTabs.packagePoliciesTabText": "統合", - "xpack.ingestManager.policyDetails.subTabs.settingsTabText": "設定", - "xpack.ingestManager.policyDetails.summary.lastUpdated": "最終更新日:", - "xpack.ingestManager.policyDetails.summary.integrations": "統合", - "xpack.ingestManager.policyDetails.summary.revision": "リビジョン", - "xpack.ingestManager.policyDetails.summary.usedBy": "使用者", - "xpack.ingestManager.policyDetails.unexceptedErrorTitle": "構成を読み込む間にエラーが発生しました", - "xpack.ingestManager.policyDetails.viewAgentListTitle": "すべてのエージェント構成を表示", - "xpack.ingestManager.policyDetails.yamlDownloadButtonLabel": "構成のダウンロード", - "xpack.ingestManager.policyDetails.yamlFlyoutCloseButtonLabel": "閉じる", - "xpack.ingestManager.policyDetails.yamlflyoutTitleWithName": "「{name}」エージェント構成の編集", - "xpack.ingestManager.policyDetails.yamlflyoutTitleWithoutName": "エージェント構成", - "xpack.ingestManager.policyDetailsPackagePolicies.createFirstButtonText": "統合の追加", - "xpack.ingestManager.policyDetailsPackagePolicies.createFirstMessage": "この構成にはまだ統合がありません。", - "xpack.ingestManager.policyDetailsPackagePolicies.createFirstTitle": "最初の統合を追加", - "xpack.ingestManager.policyForm.deletePolicyActionText": "構成を削除", - "xpack.ingestManager.policyForm.deletePolicyGroupDescription": "既存のデータは削除されません。", - "xpack.ingestManager.policyForm.deletePolicyGroupTitle": "構成を削除", - "xpack.ingestManager.policyForm.generalSettingsGroupDescription": "エージェント構成の名前と説明を選択してください。", - "xpack.ingestManager.policyForm.generalSettingsGroupTitle": "一般設定", - "xpack.ingestManager.policyForm.unableToDeleteDefaultPolicyText": "既定の構成は削除できません。", - "xpack.ingestManager.copyAgentPolicy.confirmModal.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.copyAgentPolicy.confirmModal.confirmButtonLabel": "構成をコピー", - "xpack.ingestManager.copyAgentPolicy.confirmModal.copyPolicyPrompt": "新しいエージェント構成の名前と説明を選択してください。", - "xpack.ingestManager.copyAgentPolicy.confirmModal.copyPolicyTitle": "「{name}」エージェント構成をコピー", - "xpack.ingestManager.copyAgentPolicy.confirmModal.defaultNewPolicyName": "{name}(コピー)", - "xpack.ingestManager.copyAgentPolicy.confirmModal.newDescriptionLabel": "説明", - "xpack.ingestManager.copyAgentPolicy.confirmModal.newNameLabel": "新しい構成名", - "xpack.ingestManager.copyAgentPolicy.failureNotificationTitle": "エージェント構成「{id}」のコピーエラー", - "xpack.ingestManager.copyAgentPolicy.fatalErrorNotificationTitle": "エージェント構成のコピーエラー", - "xpack.ingestManager.copyAgentPolicy.successNotificationTitle": "エージェント構成がコピーされました", - "xpack.ingestManager.createAgentPolicy.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.createAgentPolicy.errorNotificationTitle": "エージェント構成を作成できません", - "xpack.ingestManager.createAgentPolicy.flyoutTitle": "エージェント構成を作成", - "xpack.ingestManager.createAgentPolicy.flyoutTitleDescription": "エージェント構成は、エージェントのグループ全体にわたる設定を管理する目的で使用されます。エージェント構成に統合を追加すると、エージェントで収集するデータを指定できます。エージェント構成の編集時には、フリートを使用して、指定したエージェントのグループに更新をデプロイできます。", - "xpack.ingestManager.createAgentPolicy.submitButtonLabel": "エージェント構成を作成", - "xpack.ingestManager.createAgentPolicy.successNotificationTitle": "エージェント構成「{name}」を作成しました", - "xpack.ingestManager.createPackagePolicy.addedNotificationMessage": "フリートは'{agentPolicyName}'構成で使用されているすべてのエージェントに更新をデプロイします。", - "xpack.ingestManager.createPackagePolicy.addedNotificationTitle": "正常に「{packagePolicyName}」を追加しました", - "xpack.ingestManager.createPackagePolicy.agentPolicyNameLabel": "エージェント構成", - "xpack.ingestManager.createPackagePolicy.cancelButton": "キャンセル", - "xpack.ingestManager.createPackagePolicy.cancelLinkText": "キャンセル", - "xpack.ingestManager.createPackagePolicy.errorOnSaveText": "統合構成にはエラーがあります。保存前に修正してください。", - "xpack.ingestManager.createPackagePolicy.pageDescriptionfromPolicy": "選択したエージェント構成の統合を構成します。", - "xpack.ingestManager.createPackagePolicy.pageDescriptionfromPackage": "次の手順に従い、この統合をエージェント構成に追加します。", - "xpack.ingestManager.createPackagePolicy.pageTitle": "統合の追加", - "xpack.ingestManager.createPackagePolicy.pageTitleWithPackageName": "{packageName}統合の追加", - "xpack.ingestManager.createPackagePolicy.saveButton": "統合の保存", - "xpack.ingestManager.createPackagePolicy.stepConfigure.advancedOptionsToggleLinkText": "高度なオプション", - "xpack.ingestManager.createPackagePolicy.stepConfigure.errorCountText": "{count, plural, one {件のエラー} other {件のエラー}}", - "xpack.ingestManager.createPackagePolicy.stepConfigure.hideStreamsAriaLabel": "{type}入力を非表示", - "xpack.ingestManager.createPackagePolicy.stepConfigure.inputSettingsDescription": "次の設定は以下のすべての入力に適用されます。", - "xpack.ingestManager.createPackagePolicy.stepConfigure.inputSettingsTitle": "設定", - "xpack.ingestManager.createPackagePolicy.stepConfigure.inputVarFieldOptionalLabel": "オプション", - "xpack.ingestManager.createPackagePolicy.stepConfigure.integrationSettingsSectionDescription": "この統合の使用方法を識別できるように、名前と説明を選択してください。", - "xpack.ingestManager.createPackagePolicy.stepConfigure.integrationSettingsSectionTitle": "統合設定", - "xpack.ingestManager.createPackagePolicy.stepConfigure.noPolicyOptionsMessage": "構成するものがありません", - "xpack.ingestManager.createPackagePolicy.stepConfigure.packagePolicyDescriptionInputLabel": "説明", - "xpack.ingestManager.createPackagePolicy.stepConfigure.packagePolicyNameInputLabel": "統合名", - "xpack.ingestManager.createPackagePolicy.stepConfigure.packagePolicyNamespaceInputLabel": "名前空間", - "xpack.ingestManager.createPackagePolicy.stepConfigure.showStreamsAriaLabel": "{type}入力を表示", - "xpack.ingestManager.createPackagePolicy.stepConfigure.toggleAdvancedOptionsButtonText": "高度なオプション", - "xpack.ingestManager.createPackagePolicy.stepConfigurePackagePolicyTitle": "統合の構成", - "xpack.ingestManager.createPackagePolicy.stepSelectAgentPolicyTitle": "エージェント構成を選択する", - "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.addButton": "新しいエージェント構成", - "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsCountText": "{count, plural, one {# エージェント} other {# エージェント}}", - "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.errorLoadingAgentPoliciesTitle": "エージェント構成の読み込みエラー", - "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.errorLoadingPackageTitle": "パッケージ情報の読み込みエラー", - "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.errorLoadingSelectedAgentPolicyTitle": "選択したエージェント構成の読み込みエラー", - "xpack.ingestManager.createPackagePolicy.stepSelectPackage.errorLoadingPolicyTitle": "エージェント構成情報の読み込みエラー", - "xpack.ingestManager.createPackagePolicy.stepSelectPackage.errorLoadingPackagesTitle": "統合の読み込みエラー", - "xpack.ingestManager.createPackagePolicy.stepSelectPackage.errorLoadingSelectedPackageTitle": "選択した統合の読み込みエラー", - "xpack.ingestManager.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder": "統合を検索", - "xpack.ingestManager.createPackagePolicy.stepSelectPackageTitle": "統合を選択", "xpack.ingestManager.dataStreamList.actionsColumnTitle": "アクション", "xpack.ingestManager.dataStreamList.datasetColumnTitle": "データセット", "xpack.ingestManager.dataStreamList.integrationColumnTitle": "統合", @@ -9336,73 +9163,34 @@ "xpack.ingestManager.dataStreamList.viewDashboardsActionText": "ダッシュボードを表示", "xpack.ingestManager.dataStreamList.viewDashboardsPanelTitle": "ダッシュボードを表示", "xpack.ingestManager.defaultSearchPlaceholderText": "検索", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# エージェントは} other {# エージェントは}}このエージェント構成に割り当てられました。この構成を削除する前に、これらのエージェントの割り当てを解除します。", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.affectedAgentsTitle": "使用中の構成", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.confirmButtonLabel": "構成を削除", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.deletePolicyTitle": "このエージェント構成を削除しますか?", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.irreversibleMessage": "この操作は元に戻すことができません。", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.loadingAgentsCountMessage": "影響があるエージェントの数を確認中...", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.loadingButtonLabel": "読み込み中...", - "xpack.ingestManager.deleteAgentPolicy.failureSingleNotificationTitle": "エージェント構成「{id}」の削除エラー", - "xpack.ingestManager.deleteAgentPolicy.fatalErrorNotificationTitle": "エージェント構成の削除エラー", - "xpack.ingestManager.deleteAgentPolicy.successSingleNotificationTitle": "エージェント構成「{id}」を削除しました", - "xpack.ingestManager.deletePackagePolicy.confirmModal.affectedAgentsMessage": "{agentPolicyName} が一部のエージェントですでに使用されていることをフリートが検出しました。", - "xpack.ingestManager.deletePackagePolicy.confirmModal.affectedAgentsTitle": "このアクションは {agentsCount} {agentsCount, plural, one {# エージェント} other {# エージェント}}に影響します", - "xpack.ingestManager.deletePackagePolicy.confirmModal.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.deletePackagePolicy.confirmModal.confirmButtonLabel": "{agentPoliciesCount, plural, one {個の統合} other {個の統合}}を削除", - "xpack.ingestManager.deletePackagePolicy.confirmModal.deleteMultipleTitle": "{count, plural, one {個の統合} other {個の統合}}を削除しますか?", - "xpack.ingestManager.deletePackagePolicy.confirmModal.generalMessage": "このアクションは元に戻せません。続行していいですか?", - "xpack.ingestManager.deletePackagePolicy.confirmModal.loadingAgentsCountMessage": "影響があるエージェントを確認中...", - "xpack.ingestManager.deletePackagePolicy.confirmModal.loadingButtonLabel": "読み込み中...", - "xpack.ingestManager.deletePackagePolicy.failureMultipleNotificationTitle": "{count}個の統合の削除エラー", - "xpack.ingestManager.deletePackagePolicy.failureSingleNotificationTitle": "統合「{id}」の削除エラー", - "xpack.ingestManager.deletePackagePolicy.fatalErrorNotificationTitle": "統合の削除エラー", - "xpack.ingestManager.deletePackagePolicy.successMultipleNotificationTitle": "{count}個の統合を削除しました", - "xpack.ingestManager.deletePackagePolicy.successSingleNotificationTitle": "統合「{id}」を削除しました", "xpack.ingestManager.disabledSecurityDescription": "Elastic Fleet を使用するには、Kibana と Elasticsearch でセキュリティを有効にする必要があります。", "xpack.ingestManager.disabledSecurityTitle": "セキュリティが有効ではありません", - "xpack.ingestManager.editAgentPolicy.cancelButtonText": "キャンセル", - "xpack.ingestManager.editAgentPolicy.errorNotificationTitle": "エージェント構成を作成できません", - "xpack.ingestManager.editAgentPolicy.saveButtonText": "変更を保存", - "xpack.ingestManager.editAgentPolicy.savingButtonText": "保存中…", - "xpack.ingestManager.editAgentPolicy.successNotificationTitle": "正常に'{name}'設定を更新しました", - "xpack.ingestManager.editAgentPolicy.unsavedChangesText": "保存されていない変更があります", - "xpack.ingestManager.editPackagePolicy.cancelButton": "キャンセル", - "xpack.ingestManager.editPackagePolicy.errorLoadingDataMessage": "この統合情報の読み込みエラーが発生しました", - "xpack.ingestManager.editPackagePolicy.errorLoadingDataTitle": "データの読み込み中にエラーが発生", - "xpack.ingestManager.editPackagePolicy.failedConflictNotificationMessage": "データが最新ではありません。最新の構成を取得するには、ページを更新してください。", - "xpack.ingestManager.editPackagePolicy.failedNotificationTitle": "「{packagePolicyName}」の更新エラー", - "xpack.ingestManager.editPackagePolicy.pageDescription": "統合設定を修正し、選択したエージェント構成に変更をデプロイします。", - "xpack.ingestManager.editPackagePolicy.pageTitle": "統合の編集", - "xpack.ingestManager.editPackagePolicy.pageTitleWithPackageName": "{packageName}統合の編集", - "xpack.ingestManager.editPackagePolicy.saveButton": "統合の保存", - "xpack.ingestManager.editPackagePolicy.updatedNotificationMessage": "フリートは'{agentPolicyName}'構成で使用されているすべてのエージェントに更新をデプロイします。", - "xpack.ingestManager.editPackagePolicy.updatedNotificationTitle": "正常に「{packagePolicyName}」を更新しました", "xpack.ingestManager.enrollemntAPIKeyList.emptyMessage": "登録トークンが見つかりません。", "xpack.ingestManager.enrollemntAPIKeyList.loadingTokensMessage": "登録トークンを読み込んでいます...", - "xpack.ingestManager.enrollmentStepAgentPolicy.policySelectAriaLabel": "エージェント構成", - "xpack.ingestManager.enrollmentStepAgentPolicy.policySelectLabel": "エージェント構成", - "xpack.ingestManager.enrollmentStepAgentPolicy.enrollmentTokenSelectLabel": "登録トークン", - "xpack.ingestManager.enrollmentStepAgentPolicy.showAuthenticationSettingsButton": "認証設定", + "xpack.ingestManager.enrollmentInstructions.descriptionText": "エージェントのディレクトリから、該当するコマンドを実行し、Elasticエージェントを登録して起動します。再度これらのコマンドを実行すれば、複数のコンピューターでエージェントを設定できます。登録ステップは必ずシステムで管理者権限をもつユーザーとして実行してください。", + "xpack.ingestManager.enrollmentInstructions.linuxDebRpmTitle": "Linux(.debおよび.rpm)", + "xpack.ingestManager.enrollmentInstructions.macLinuxTarInstructions": "エージェントのシステムが再起動する場合は、{command}を実行する必要があります。", + "xpack.ingestManager.enrollmentInstructions.macLinuxTarTitle": "macOS / Linux (.tar.gz)", + "xpack.ingestManager.enrollmentInstructions.windowsTitle": "Windows", "xpack.ingestManager.enrollmentTokenDeleteModal.cancelButton": "キャンセル", "xpack.ingestManager.enrollmentTokenDeleteModal.deleteButton": "削除", "xpack.ingestManager.enrollmentTokenDeleteModal.description": "{keyName}を削除してよろしいですか?", "xpack.ingestManager.enrollmentTokenDeleteModal.title": "登録トークンを削除", "xpack.ingestManager.enrollmentTokensList.actionsTitle": "アクション", "xpack.ingestManager.enrollmentTokensList.activeTitle": "アクティブ", - "xpack.ingestManager.enrollmentTokensList.policyTitle": "エージェント構成", "xpack.ingestManager.enrollmentTokensList.createdAtTitle": "作成日時", + "xpack.ingestManager.enrollmentTokensList.hideTokenButtonLabel": "トークンを非表示", "xpack.ingestManager.enrollmentTokensList.nameTitle": "名前", "xpack.ingestManager.enrollmentTokensList.newKeyButton": "新しい登録トークン", "xpack.ingestManager.enrollmentTokensList.pageDescription": "これは、エージェントを登録するために使用できる登録トークンのリストです。", + "xpack.ingestManager.enrollmentTokensList.revokeTokenButtonLabel": "トークンを取り消す", "xpack.ingestManager.enrollmentTokensList.secretTitle": "シークレット", - "xpack.ingestManager.epm.addPackagePolicyButtonText": "{packageName}の追加", + "xpack.ingestManager.enrollmentTokensList.showTokenButtonLabel": "トークンを表示", + "xpack.ingestManager.epm.assetGroupTitle": "{assetType}アセット", "xpack.ingestManager.epm.browseAllButtonText": "すべての統合を参照", "xpack.ingestManager.epm.illustrationAltText": "統合の例", "xpack.ingestManager.epm.loadingIntegrationErrorTitle": "統合詳細の読み込みエラー", "xpack.ingestManager.epm.packageDetailsNav.overviewLinkText": "概要", - "xpack.ingestManager.epm.packageDetailsNav.packagePoliciesLinkText": "使用", "xpack.ingestManager.epm.packageDetailsNav.settingsLinkText": "設定", "xpack.ingestManager.epm.pageSubtitle": "一般的なアプリやサービスの統合を参照する", "xpack.ingestManager.epm.pageTitle": "統合", @@ -9481,7 +9269,6 @@ "xpack.ingestManager.metadataForm.submitButtonText": "追加", "xpack.ingestManager.metadataForm.valueLabel": "値", "xpack.ingestManager.newEnrollmentKey.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.newEnrollmentKey.policyLabel": "構成", "xpack.ingestManager.newEnrollmentKey.flyoutTitle": "新しい登録トークンを作成", "xpack.ingestManager.newEnrollmentKey.keyCreatedToasts": "登録トークンが作成されました。", "xpack.ingestManager.newEnrollmentKey.nameLabel": "名前", @@ -9492,17 +9279,12 @@ "xpack.ingestManager.overviewAgentErrorTitle": "エラー", "xpack.ingestManager.overviewAgentOfflineTitle": "オフライン", "xpack.ingestManager.overviewAgentTotalTitle": "合計エージェント数", - "xpack.ingestManager.overviewPolicyTotalTitle": "合計利用可能数", "xpack.ingestManager.overviewDatastreamNamespacesTitle": "名前空間", "xpack.ingestManager.overviewDatastreamSizeTitle": "合計サイズ", "xpack.ingestManager.overviewDatastreamTotalTitle": "データセット", "xpack.ingestManager.overviewIntegrationsInstalledTitle": "インストール済み", "xpack.ingestManager.overviewIntegrationsTotalTitle": "合計利用可能数", "xpack.ingestManager.overviewIntegrationsUpdatesAvailableTitle": "更新が可能です", - "xpack.ingestManager.overviewPackagePolicyTitle": "構成された統合", - "xpack.ingestManager.overviewPagePoliciesPanelAction": "構成を表示", - "xpack.ingestManager.overviewPagePoliciesPanelTitle": "エージェント構成", - "xpack.ingestManager.overviewPagePoliciesPanelTooltip": "エージェント構成を使用すると、エージェントが収集するデータを管理できます。", "xpack.ingestManager.overviewPageDataStreamsPanelAction": "データセットを表示", "xpack.ingestManager.overviewPageDataStreamsPanelTitle": "データセット", "xpack.ingestManager.overviewPageDataStreamsPanelTooltip": "エージェントが収集するデータはさまざまなデータセットに整理されます。", @@ -9515,11 +9297,6 @@ "xpack.ingestManager.overviewPageIntegrationsPanelTooltip": "Elastic Stackの統合を参照し、インストールします。統合をエージェント構成に追加し、データの送信を開始します。", "xpack.ingestManager.overviewPageSubtitle": "Elasticエージェントおよびエージェント構成の集中管理。", "xpack.ingestManager.overviewPageTitle": "Ingest Manager", - "xpack.ingestManager.packagePolicyValidation.invalidArrayErrorMessage": "無効なフォーマット", - "xpack.ingestManager.packagePolicyValidation.invalidYamlFormatErrorMessage": "YAML形式が無効です", - "xpack.ingestManager.packagePolicyValidation.nameRequiredErrorMessage": "名前が必要です", - "xpack.ingestManager.packagePolicyValidation.namespaceRequiredErrorMessage": "ネームスペースが必要です", - "xpack.ingestManager.packagePolicyValidation.requiredErrorMessage": "{fieldName}が必要です", "xpack.ingestManager.permissionDeniedErrorMessage": "Ingest Managerにアクセスする権限がありません。Ingest Managerには{roleName}権限が必要です。", "xpack.ingestManager.permissionDeniedErrorTitle": "パーミッションが拒否されました", "xpack.ingestManager.permissionsRequestErrorMessageDescription": "Ingest Managerアクセス権の確認中に問題が発生しました", @@ -9564,8 +9341,10 @@ "xpack.ingestManager.unenrollAgents.confirmModal.confirmButtonLabel": "登録解除", "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "{count, plural, one {# エージェント} other {# エージェント}}の登録を解除しますか?", "xpack.ingestManager.unenrollAgents.confirmModal.deleteSingleTitle": "エージェント「{id}」の登録を解除しますか?", + "xpack.ingestManager.unenrollAgents.confirmModal.forceDeleteSingleTitle": "強制的にエージェント「{id}」の登録を解除しますか?", "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "読み込み中...", "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "エージェントの登録解除エラー", + "xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle": "エージェント「{id}」の登録を解除しました", "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "エージェント「{id}」を登録解除しています", "xpack.ingestPipelines.app.checkingPrivilegesDescription": "権限を確認中…", "xpack.ingestPipelines.app.checkingPrivilegesErrorMessage": "サーバーからユーザー特権を取得中にエラーが発生。", @@ -9689,13 +9468,12 @@ "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.cancelButtonLabel": "キャンセル", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.confirmationButtonLabel": "プロセッサーの削除", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.titleText": "{type}プロセッサーの削除", - "xpack.ingestPipelines.pipelineEditor.setForm.fieldFieldLabel": "フィールド", - "xpack.ingestPipelines.pipelineEditor.setForm.fieldRequiredError": "フィールド値が必要です。", "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel": "無効化", "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel": "値", "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "設定する値が必要です。", "xpack.ingestPipelines.pipelineEditor.settingsForm.learnMoreLabelLink.processor": "{processorLabel}ドキュメント", "xpack.ingestPipelines.pipelineEditor.typeField.fieldRequiredError": "タイプが必要です。", + "xpack.ingestPipelines.pipelineEditor.typeField.typeFieldComboboxPlaceholder": "入力してエンターキーを押してください", "xpack.ingestPipelines.pipelineEditor.typeField.typeFieldLabel": "プロセッサー", "xpack.ingestPipelines.processors.label.append": "末尾に追加", "xpack.ingestPipelines.processors.label.bytes": "バイト", @@ -9737,6 +9515,7 @@ "xpack.ingestPipelines.requestFlyout.unnamedTitle": "リクエスト", "xpack.ingestPipelines.settingsFormOnFailureFlyout.addButtonLabel": "追加", "xpack.ingestPipelines.settingsFormOnFailureFlyout.cancelButtonLabel": "キャンセル", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.updateButtonLabel": "更新", "xpack.ingestPipelines.tabs.outputTabTitle": "アウトプット", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel": "ドキュメント", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError": "ドキュメントJSONが無効です。", @@ -9776,7 +9555,6 @@ "xpack.lens.configure.editConfig": "構成の編集", "xpack.lens.configure.emptyConfig": "ここにフィールドをドロップ", "xpack.lens.dataPanelWrapper.switchDatasource": "データソースに切り替える", - "xpack.lens.datatable.columns": "フィールド", "xpack.lens.datatable.conjunctionSign": " と ", "xpack.lens.datatable.expressionHelpLabel": "データベースレンダー", "xpack.lens.datatable.label": "データテーブル", @@ -9822,6 +9600,7 @@ "xpack.lens.functions.renameColumns.idMap.help": "キーが古い列 ID で値が対応する新しい列 ID となるように JSON エンコーディングされたオブジェクトです。他の列 ID はすべてのそのままです。", "xpack.lens.includeValueButtonAriaLabel": "{value}を含める", "xpack.lens.includeValueButtonTooltip": "値を含める", + "xpack.lens.indexPattern.allFieldsLabel": "すべてのフィールド", "xpack.lens.indexPattern.availableFieldsLabel": "利用可能なフィールド", "xpack.lens.indexPattern.avg": "平均", "xpack.lens.indexPattern.avgOf": "{name} の平均", @@ -9849,6 +9628,8 @@ "xpack.lens.indexPattern.defaultFormatLabel": "デフォルト", "xpack.lens.indexPattern.emptyFieldsLabel": "空のフィールド", "xpack.lens.indexpattern.emptyTextColumnValue": "(空)", + "xpack.lens.indexPattern.existenceErrorAriaLabel": "存在の取り込みに失敗しました", + "xpack.lens.indexPattern.existenceErrorLabel": "フィールド情報を読み込めません", "xpack.lens.indexPattern.fieldDistributionLabel": "分布", "xpack.lens.indexPattern.fieldItemTooltip": "可視化するには、ドラッグアンドドロップします。", "xpack.lens.indexPattern.fieldlessOperationLabel": "この関数を使用するには、フィールドを選択してください。", @@ -9857,6 +9638,7 @@ "xpack.lens.indexPattern.fieldStatsButtonLabel": "フィールドプレビューを表示するには、クリックします。可視化するには、ドラッグアンドドロップします。", "xpack.lens.indexPattern.fieldStatsCountLabel": "カウント", "xpack.lens.indexPattern.fieldStatsDisplayToggle": "次のどちらかを切り替えます:", + "xpack.lens.indexPattern.fieldStatsNoData": "表示するデータがありません", "xpack.lens.indexPattern.fieldTimeDistributionLabel": "時間分布", "xpack.lens.indexPattern.fieldTopValuesLabel": "トップの値", "xpack.lens.indexPattern.groupByDropdown": "グループ分けの条件", @@ -10163,7 +9945,7 @@ "xpack.maps.aggs.defaultCountLabel": "カウント", "xpack.maps.appTitle": "マップ", "xpack.maps.blendedVectorLayer.clusteredLayerName": "クラスター化 {displayName}", - "xpack.maps.breadCrumbs.unsavedChangesWarning": "保存されていない変更は保存されない可能性があります", + "xpack.maps.breadCrumbs.unsavedChangesWarning": "マップには保存されていない変更があります。終了してよろしいですか?", "xpack.maps.choropleth.boundaries.elasticsearch": "Elasticsearchの点、線、多角形", "xpack.maps.choropleth.boundaries.ems": "Elastic Maps Serviceの行政区画のベクターシェイプ", "xpack.maps.choropleth.boundariesLabel": "境界ソース", @@ -10492,12 +10274,12 @@ "xpack.maps.source.kbnTMSDescription": "kibana.yml で構成されたマップタイルです", "xpack.maps.source.kbnTMSTitle": "カスタムタイルマップサービス", "xpack.maps.source.mapSettingsPanel.initialLocationLabel": "マップの初期位置情報", - "xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle": ".pbfベクトルタイル", + "xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle": "ベクトルタイル", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "ズーム:", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsMessage": "フィールド", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsPostHelpMessage": "これらはツールチップと動的スタイルで使用できます。", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsPreHelpMessage": "使用可能なフィールド ", - "xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "タイルレイヤー", + "xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "ソースレイヤー", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlHelpMessage": ".mvtベクトルタイルサービスのURL。例:{url}", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlMessage": "Url", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.zoomRangeHelpMessage": "レイヤーがタイルに存在するズームレベル。これは直接可視性に対応しません。レベルが低いレイヤーデータは常に高いズームレベルで表示できます(ただし逆はありません)。", @@ -10634,7 +10416,9 @@ "xpack.maps.visTypeAlias.legacyMapVizWarning": "座標マップと地域マップの代わりに、マップアプリを使用します。\nマップアプリはより機能が豊富で、使いやすくなっています。", "xpack.maps.visTypeAlias.title": "マップ", "xpack.maps.xyztmssource.attributionLink": "属性テキストにはリンクが必要です", + "xpack.maps.xyztmssource.attributionLinkLabel": "属性リンク", "xpack.maps.xyztmssource.attributionText": "属性 URL にはテキストが必要です", + "xpack.maps.xyztmssource.attributionTextLabel": "属性テキスト", "xpack.ml.accessDenied.description": "ML プラグインへのアクセスパーミッションがありません", "xpack.ml.accessDenied.label": "パーミッションがありません", "xpack.ml.accessDeniedLabel": "アクセスが拒否されました", @@ -10910,7 +10694,7 @@ "xpack.ml.dataframe.analytics.create.calloutMessage": "分析フィールドを読み込むには追加のデータが必要です。", "xpack.ml.dataframe.analytics.create.calloutTitle": "分析フィールドがありません", "xpack.ml.dataframe.analytics.create.chooseSourceTitle": "ソースインデックスパターンを選択してください", - "xpack.ml.dataframe.analytics.create.classificationHelpText": "分類ジョブには表のようなデータ構造でマッピングされたソースインデックスが必要で、数値、ブール値、テキスト、キーワード、またはIPフィールドのみがサポートされます。予測フィールド名などのカスタムオプションを適用するには、詳細エディターを使用します。", + "xpack.ml.dataframe.analytics.create.classificationHelpText": "分類はデータセットのデータポイントのラベルを予測します。", "xpack.ml.dataframe.analytics.create.computeFeatureInfluenceFalseValue": "False", "xpack.ml.dataframe.analytics.create.computeFeatureInfluenceLabel": "演算機能影響", "xpack.ml.dataframe.analytics.create.computeFeatureInfluenceLabelHelpText": "機能影響演算が有効かどうかを指定します。デフォルトはtrueです。", @@ -10959,6 +10743,7 @@ "xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "編集", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "Kibanaインデックスパターンの作成中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "インデックスパターン{indexPatternName}はすでに作成されています。", + "xpack.ml.dataframe.analytics.create.errorCheckingIndexExists": "既存のインデックス名の取得中に次のエラーが発生しました:{error}", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "データフレーム分析ジョブの作成中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "既存のデータフレーム分析ジョブIDの取得中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました。", @@ -10974,8 +10759,8 @@ "xpack.ml.dataframe.analytics.create.gammaInputAriaLabel": "フォレストの個別のツリーのサイズに関連付けられた線形ペナルティを乗算します", "xpack.ml.dataframe.analytics.create.gammaLabel": "ガンマ", "xpack.ml.dataframe.analytics.create.gammaText": "フォレストの個別のツリーのサイズに関連付けられた線形ペナルティを乗算します。非負の値でなければなりません。", - "xpack.ml.dataframe.analytics.create.hyperParametersDetailsTitle": "ハイパーパラメーター", - "xpack.ml.dataframe.analytics.create.hyperParametersSectionTitle": "ハイパーパラメーター", + "xpack.ml.dataframe.analytics.create.hyperParametersDetailsTitle": "ハイパーパラメータ", + "xpack.ml.dataframe.analytics.create.hyperParametersSectionTitle": "ハイパーパラメータ", "xpack.ml.dataframe.analytics.create.includedFieldsCount": "{numFields, plural, one {個のフィールド} other {個のフィールド}}が分析に含まれます", "xpack.ml.dataframe.analytics.create.includedFieldsLabel": "含まれるフィールド", "xpack.ml.dataframe.analytics.create.indexPatternAlreadyExistsError": "このタイトルのインデックスパターンが既に存在します。", @@ -11019,11 +10804,11 @@ "xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesHelpText": "返すドキュメントごとに機能重要度値の最大数を指定します。", "xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesInputAriaLabel": "ドキュメントごとの機能重要度値の最大数。", "xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesLabel": "機能重要度値", - "xpack.ml.dataframe.analytics.create.outlierDetectionHelpText": "外れ値検出ジョブは、表に示すようなデータ構造でマッピングされたソースインデックスを必要とし、数字とブール値フィールドのみを分析します。カスタムオプションを構成に追加するには、詳細エディターを使用します。", + "xpack.ml.dataframe.analytics.create.outlierDetectionHelpText": "異常値検出により、データセットにおける異常なデータポイントが特定されます。", "xpack.ml.dataframe.analytics.create.outlierFractionHelpText": "異常値検出の前に異常であると想定されるデータセットの比率を設定します。", "xpack.ml.dataframe.analytics.create.outlierFractionInputAriaLabel": "異常値検出の前に異常であると想定されるデータセットの比率を設定します。", "xpack.ml.dataframe.analytics.create.outlierFractionLabel": "異常値割合", - "xpack.ml.dataframe.analytics.create.outlierRegressionHelpText": "リグレッションジョブは数値フィールドのみを分析します。予測フィールド名などのカスタムオプションを適用するには、詳細エディターを使用します。", + "xpack.ml.dataframe.analytics.create.outlierRegressionHelpText": "回帰はデータセットにおける数値を予測します。", "xpack.ml.dataframe.analytics.create.predictionFieldNameHelpText": "結果で予測フィールドの名前を定義します。デフォルトは_predictionです。", "xpack.ml.dataframe.analytics.create.predictionFieldNameLabel": "予測フィールド名", "xpack.ml.dataframe.analytics.create.randomizeSeedInputAriaLabel": "学習で使用されるドキュメントを選択するために使用される乱数生成器のシード", @@ -11102,6 +10887,7 @@ "xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle": "トレーニングエラー", "xpack.ml.dataframe.analytics.regressionExploration.trainingFilterText": ".テストデータをフィルタリングしています。", "xpack.ml.dataframe.analyticsList.analyticsDetails.tabs.analyticsMessagesLabel": "ジョブメッセージ", + "xpack.ml.dataframe.analyticsList.cloneActionPermissionTooltip": "分析ジョブを複製する権限がありません。", "xpack.ml.dataframe.analyticsList.completeBatchAnalyticsToolTip": "{analyticsId}は完了済みの分析ジョブで、再度開始できません。", "xpack.ml.dataframe.analyticsList.createDataFrameAnalyticsButton": "ジョブを作成", "xpack.ml.dataframe.analyticsList.deleteActionDisabledToolTipContent": "削除するにはデータフレーム分析ジョブを停止してください。", @@ -11171,7 +10957,11 @@ "xpack.ml.dataframe.analyticsList.title": "データフレーム分析ジョブ", "xpack.ml.dataframe.analyticsList.type": "タイプ", "xpack.ml.dataframe.analyticsList.typeFilter": "タイプ", + "xpack.ml.dataframe.analyticsList.viewActionJobFailedToolTipContent": "データフレーム分析ジョブに失敗しました。結果ページがありません。", + "xpack.ml.dataframe.analyticsList.viewActionJobNotFinishedToolTipContent": "データフレーム分析ジョブは完了していません。結果ページがありません。", + "xpack.ml.dataframe.analyticsList.viewActionJobNotStartedToolTipContent": "データフレーム分析ジョブが開始しませんでした。結果ページがありません。", "xpack.ml.dataframe.analyticsList.viewActionName": "表示", + "xpack.ml.dataframe.analyticsList.viewActionUnknownJobTypeToolTipContent": "このタイプのデータフレーム分析ジョブでは、結果ページがありません。", "xpack.ml.dataframe.stepCreateForm.createDataFrameAnalyticsSuccessMessage": "データフレーム分析 {jobId} の作成リクエストが受け付けられました。", "xpack.ml.dataframe.stepDetailsForm.destinationIndexInvalidErrorLink": "インデックス名の制限に関する詳細。", "xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel": "探索", @@ -11250,6 +11040,8 @@ "xpack.ml.explorer.addToDashboard.selectDashboardsLabel": "ダッシュボードを選択:", "xpack.ml.explorer.addToDashboard.selectSwimlanesLabel": "スイムレーンビューを選択:", "xpack.ml.explorer.addToDashboardLabel": "ダッシュボードに追加", + "xpack.ml.explorer.annotationsErrorCallOutTitle": "注釈の読み込み中にエラーが発生しました。", + "xpack.ml.explorer.annotationsErrorTitle": "注釈", "xpack.ml.explorer.annotationsTitle": "注釈{badge}", "xpack.ml.explorer.annotationsTitleTotalCount": "合計:{count}", "xpack.ml.explorer.anomaliesTitle": "異常", @@ -11379,7 +11171,7 @@ "xpack.ml.fileDatavisualizer.analysisSummary.timeFormatTitle": "時間 {timestampFormats, plural, zero {format} one {format} other {formats}}", "xpack.ml.fileDatavisualizer.bottomBar.backButtonLabel": "戻る", "xpack.ml.fileDatavisualizer.bottomBar.cancelButtonLabel": "キャンセル", - "xpack.ml.fileDatavisualizer.bottomBar.missingImportPrivilegesMessage": "データをインポートするために必要な権限がありません", + "xpack.ml.fileDatavisualizer.bottomBar.missingImportPrivilegesMessage": "データインポートを有効にするには、ingest_adminロールが必要です", "xpack.ml.fileDatavisualizer.bottomBar.readMode.cancelButtonLabel": "キャンセル", "xpack.ml.fileDatavisualizer.bottomBar.readMode.importButtonLabel": "インポート", "xpack.ml.fileDatavisualizer.editFlyout.applyOverrideSettingsButtonLabel": "適用", @@ -11896,6 +11688,7 @@ "xpack.ml.navMenu.overviewTabLinkText": "概要", "xpack.ml.navMenu.settingsTabLinkText": "設定", "xpack.ml.newJob.page.createJob": "ジョブを作成", + "xpack.ml.newJob.page.createJob.indexPatternTitle": "インデックスパターン{index}の使用", "xpack.ml.newJob.recognize.advancedLabel": "高度な設定", "xpack.ml.newJob.recognize.advancedSettingsAriaLabel": "高度な設定", "xpack.ml.newJob.recognize.alreadyExistsLabel": "(既に存在します)", @@ -12437,6 +12230,8 @@ "xpack.ml.timeSeriesExplorer.annotationFlyout.maxLengthError": "最長 {maxChars} 文字を {charsOver, number} {charsOver, plural, one {文字} other {文字}} 超過", "xpack.ml.timeSeriesExplorer.annotationFlyout.noAnnotationTextError": "注釈テキストを入力してください", "xpack.ml.timeSeriesExplorer.annotationFlyout.updateButtonLabel": "更新", + "xpack.ml.timeSeriesExplorer.annotationsErrorCallOutTitle": "注釈の読み込み中にエラーが発生しました。", + "xpack.ml.timeSeriesExplorer.annotationsErrorTitle": "注釈", "xpack.ml.timeSeriesExplorer.annotationsLabel": "注釈", "xpack.ml.timeSeriesExplorer.annotationsTitle": "注釈{badge}", "xpack.ml.timeSeriesExplorer.anomaliesTitle": "異常", @@ -12689,6 +12484,7 @@ "xpack.monitoring.alerts.nodesChanged.resolved.internalShortMessage": "{clusterName}のElasticsearchノード変更アラートが解決されました。", "xpack.monitoring.alerts.nodesChanged.shortAction": "ノードを追加、削除、または再起動したことを確認してください。", "xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage": "Elasticsearchノード「{added}」がこのクラスターに追加されました。", + "xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage": "Elasticsearchノードが変更されました", "xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage": "Elasticsearchノード「{removed}」がこのクラスターから削除されました。", "xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage": "このクラスターのElasticsearchノードは変更されていません。", "xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage": "このクラスターでElasticsearchノード「{restarted}」が再起動しました。", @@ -12970,6 +12766,7 @@ "xpack.monitoring.elasticsearch.node.advanced.routeTitle": "Elasticsearch - ノード - {nodeSummaryName} - 高度な設定", "xpack.monitoring.elasticsearch.node.overview.routeTitle": "Elasticsearch - ノード - {nodeName} - 概要", "xpack.monitoring.elasticsearch.node.statusIconLabel": "ステータス: {status}", + "xpack.monitoring.elasticsearch.nodeDetailStatus.alerts": "アラート", "xpack.monitoring.elasticsearch.nodeDetailStatus.dataLabel": "データ", "xpack.monitoring.elasticsearch.nodeDetailStatus.documentsLabel": "ドキュメント", "xpack.monitoring.elasticsearch.nodeDetailStatus.freeDiskSpaceLabel": "空きディスク容量", @@ -13072,6 +12869,9 @@ "xpack.monitoring.febLabel": "2月", "xpack.monitoring.formatNumbers.notAvailableLabel": "N/A", "xpack.monitoring.friLabel": "金", + "xpack.monitoring.healthCheck.encryptionErrorAction": "方法を確認してください。", + "xpack.monitoring.healthCheck.tlsAndEncryptionError": "アラート機能を使用するには、KibanaとElasticsearchとの間のトランスポート層セキュリティを有効化して、 \n kibana.ymlファイルで暗号化鍵を構成する必要があります。", + "xpack.monitoring.healthCheck.tlsAndEncryptionErrorTitle": "追加の設定が必要です", "xpack.monitoring.janLabel": "1月", "xpack.monitoring.julLabel": "7月", "xpack.monitoring.junLabel": "6月", @@ -13934,7 +13734,7 @@ "xpack.observability.emptySection.apps.logs.link": "Filebeatをインストール", "xpack.observability.emptySection.apps.logs.title": "ログ", "xpack.observability.emptySection.apps.metrics.description": "インフラストラクチャ、アプリ、サービスからメトリックを分析します。傾向、予測動作を発見し、異常などに関するアラートを通知します。", - "xpack.observability.emptySection.apps.metrics.link": "メトリックモジュールのインストール", + "xpack.observability.emptySection.apps.metrics.link": "Metricbeatのインストール", "xpack.observability.emptySection.apps.metrics.title": "メトリック", "xpack.observability.emptySection.apps.uptime.description": "サイトとサービスの可用性をアクティブに監視するアラートを受信し、問題をより迅速に解決して、ユーザーエクスペリエンスを最適化します。", "xpack.observability.emptySection.apps.uptime.link": "Heartbeatのインストール", @@ -13946,6 +13746,10 @@ "xpack.observability.home.sectionsubtitle": "ログ、メトリック、トレースを大規模に、1つのスタックにまとめて、環境内のあらゆる場所で生じるイベントの監視、分析、対応を行います。", "xpack.observability.home.sectionTitle": "エコシステム全体の一元的な可視性", "xpack.observability.home.title": "オブザーバビリティ", + "xpack.observability.ingestManager.beta": "ベータ", + "xpack.observability.ingestManager.button": "Ingest Managerベータを試す", + "xpack.observability.ingestManager.text": "Elasticエージェントでは、シンプルかつ統合された方法で、ログ、メトリック、他の種類のデータの監視をホストに追加することができます。複数のBeatsと他のエージェントをインストールする必要はありません。このため、インフラストラクチャ全体での構成のデプロイが簡単で高速になりました。", + "xpack.observability.ingestManager.title": "新しいIngest Managerをご覧になりましたか?", "xpack.observability.landing.breadcrumb": "はじめて使う", "xpack.observability.news.readFullStory": "詳細なストーリーを読む", "xpack.observability.news.title": "新機能", @@ -15007,7 +14811,7 @@ "xpack.securitySolution.add_filter_to_global_search_bar.filterOutValueHoverAction": "値を除外", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "このルールで生成されたすべてのアラートのリスクスコアを選択します。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "デフォルトリスクスコア", - "xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel": "ソースイベント(スケール1-100)からリスクスコアにフィールドをマッピングします。", + "xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel": "ソースイベント値を使用して、デフォルトリスクスコアを上書きします。", "xpack.securitySolution.alerts.riskScoreMapping.mappingDetailsLabel": "値が境界外の場合、またはフィールドがない場合は、デフォルトリスクスコアが使用されます。", "xpack.securitySolution.alerts.riskScoreMapping.riskScoreFieldTitle": "signal.rule.risk_score", "xpack.securitySolution.alerts.riskScoreMapping.riskScoreMappingTitle": "リスクスコア無効化", @@ -15015,7 +14819,7 @@ "xpack.securitySolution.alerts.riskScoreMapping.sourceFieldTitle": "ソースフィールド", "xpack.securitySolution.alerts.severityMapping.defaultDescriptionLabel": "このルールで生成されたすべてのアラートの重要度sレベルを選択します。", "xpack.securitySolution.alerts.severityMapping.defaultSeverityTitle": "深刻度", - "xpack.securitySolution.alerts.severityMapping.mappingDescriptionLabel": "ソースイベントから特定の重要度に値をマッピングします。", + "xpack.securitySolution.alerts.severityMapping.mappingDescriptionLabel": "ソースイベント値を使用して、デフォルトの重要度を上書きします。", "xpack.securitySolution.alerts.severityMapping.mappingDetailsLabel": "複数の一致がある場合、最高重要度の一致が適用されます。一致がない場合は、デフォルト重要度が使用されます。", "xpack.securitySolution.alerts.severityMapping.severityMappingTitle": "重要度無効化", "xpack.securitySolution.alerts.severityMapping.severityTitle": "デフォルト重要度", @@ -15351,8 +15155,8 @@ "xpack.securitySolution.case.connectors.resilient.orgId": "組織ID", "xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField": "APIキーIDが必要です", "xpack.securitySolution.case.connectors.resilient.requiredApiKeySecretTextField": "APIキーシークレットが必要です", - "xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField": "組織ID", - "xpack.securitySolution.case.connectors.resilient.selectMessageText": "resilientでSIEMケースデータを更新するか、新しいインシデントにプッシュ", + "xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField": "組織IDが必要です", + "xpack.securitySolution.case.connectors.resilient.selectMessageText": "Resilientでセキュリティケースデータを更新するか、新しいインシデントにプッシュ", "xpack.securitySolution.case.createCase.descriptionFieldRequiredError": "説明が必要です。", "xpack.securitySolution.case.createCase.fieldTagsHelpText": "このケースの1つ以上のカスタム識別タグを入力します。新しいタグを開始するには、各タグの後でEnterを押します。", "xpack.securitySolution.case.createCase.titleFieldRequiredError": "タイトルが必要です。", @@ -15365,8 +15169,8 @@ "xpack.securitySolution.chart.allOthersGroupingLabel": "その他すべて", "xpack.securitySolution.chart.dataAllValuesZerosTitle": "すべての値はゼロを返します", "xpack.securitySolution.chart.dataNotAvailableTitle": "チャートデータが利用できません", - "xpack.securitySolution.chrome.help.appName": "SIEM", - "xpack.securitySolution.chrome.helpMenu.documentation": "SIEMドキュメンテーション", + "xpack.securitySolution.chrome.help.appName": "セキュリティ", + "xpack.securitySolution.chrome.helpMenu.documentation": "セキュリティドキュメント", "xpack.securitySolution.chrome.helpMenu.documentation.ecs": "ECSドキュメンテーション", "xpack.securitySolution.clipboard.copied": "コピー完了", "xpack.securitySolution.clipboard.copy": "コピー", @@ -15426,7 +15230,7 @@ "xpack.securitySolution.components.mlPopup.errors.createJobFailureTitle": "ジョブ作成エラー", "xpack.securitySolution.components.mlPopup.errors.startJobFailureTitle": "ジョブ開始エラー", "xpack.securitySolution.components.mlPopup.hooks.errors.indexPatternFetchFailureTitle": "インデックスパターン取得エラー", - "xpack.securitySolution.components.mlPopup.hooks.errors.siemJobFetchFailureTitle": "SIEMジョブ取得エラー", + "xpack.securitySolution.components.mlPopup.hooks.errors.siemJobFetchFailureTitle": "セキュリティジョブ取得エラー", "xpack.securitySolution.components.mlPopup.jobsTable.createCustomJobButtonLabel": "カスタムジョブを作成", "xpack.securitySolution.components.mlPopup.jobsTable.jobNameColumn": "ジョブ名", "xpack.securitySolution.components.mlPopup.jobsTable.noItemsDescription": "SIEM機械学習ジョブが見つかりませんでした", @@ -15504,7 +15308,7 @@ "xpack.securitySolution.dataProviders.valueAriaLabel": "値", "xpack.securitySolution.dataProviders.valuePlaceholder": "値", "xpack.securitySolution.detectionEngine.alerts.actions.addEndpointException": "エンドポイント例外の追加", - "xpack.securitySolution.detectionEngine.alerts.actions.addException": "例外の追加", + "xpack.securitySolution.detectionEngine.alerts.actions.addException": "ルール例外の追加", "xpack.securitySolution.detectionEngine.alerts.actions.closeAlertTitle": "アラートを閉じる", "xpack.securitySolution.detectionEngine.alerts.actions.inProgressAlertTitle": "実行中に設定", "xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineTitle": "タイムラインで調査", @@ -15551,7 +15355,7 @@ "xpack.securitySolution.detectionEngine.alerts.utilityBar.selectedAlertsTitle": "Selected {selectedAlertsFormatted} {selectedAlerts, plural, =1 {件のアラート} other {件のアラート}}", "xpack.securitySolution.detectionEngine.alerts.utilityBar.showingAlertsTitle": "すべての{totalAlertsFormatted} {totalAlerts, plural, =1 {件のアラート} other {件のアラート}}を表示しています", "xpack.securitySolution.detectionEngine.alerts.utilityBar.takeActionTitle": "アクションを実行", - "xpack.securitySolution.detectionEngine.alertTitle": "外部アラート", + "xpack.securitySolution.detectionEngine.alertTitle": "検出アラート", "xpack.securitySolution.detectionEngine.buttonManageRules": "検出ルールの管理", "xpack.securitySolution.detectionEngine.components.importRuleModal.cancelTitle": "キャンセル", "xpack.securitySolution.detectionEngine.components.importRuleModal.importFailedDetailedTitle": "ルールID: {ruleId}\n ステータスコード: {statusCode}\n メッセージ: {message}", @@ -15574,7 +15378,7 @@ "xpack.securitySolution.detectionEngine.createRule.savedIdLabel": "保存されたクエリ名", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.descriptionFieldRequiredError": "説明が必要です。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fiedIndexPatternsLabel": "インデックスパターン", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAssociatedToEndpointListLabel": "ルールをグローバルエンドポイント例外リストに関連付ける", + "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAssociatedToEndpointListLabel": "既存のエンドポイント例外をルールに追加", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAuthorHelpText": "このルールの作成者を1つ以上入力します。新しい作成者を追加するには、各作成者の後でEnterを押します。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAuthorLabel": "作成者", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldBuildingBlockLabel": "すべての生成されたアラートを「基本」アラートに設定", @@ -15596,7 +15400,7 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel": "タイムラインテンプレート", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimestampOverrideHelpText": "ルールを実行するときに使用されるタイムスタンプフィールドを選択します。入力時間(例:event.ingested)に最も近いタイムスタンプのフィールドを選択します。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimestampOverrideLabel": "タイムスタンプ無効化", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideHelpText": "シグナル調査を実施するアナリストに役立つ情報を提供します。このガイドは、ルールの詳細ページとこのルールで生成されたアラートから作成されたタイムラインの両方に表示されます。", + "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideHelpText": "検出アラートの調査を実施するアナリストに役立つ情報を提供します。このガイドは、ルールの詳細ページとこのルールで生成された検出アラートから(メモとして)作成されたタイムラインに表示されます。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel": "調査ガイド", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名前が必要です。", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "ルール調査ガイドを追加...", @@ -15604,7 +15408,7 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription": "参照URLを追加します", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.advancedSettingsButton": "高度な設定", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.buildingBlockLabel": "基本", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.endpointExceptionListLabel": "グローバルエンドポイント例外リスト", + "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.endpointExceptionListLabel": "Elastic Endpoint例外", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionCriticalDescription": "重大", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription": "高", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription": "低", @@ -15622,7 +15426,7 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesCustomDescription": "インデックスのカスタムリストを入力します", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesFromConfigDescription": "セキュリティソリューション詳細設定からElasticsearchインデックスを使用します", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesHelperDescription": "このルールを実行するElasticsearchインデックスのパターンを入力しますデフォルトでは、セキュリティソリューション詳細設定で定義されたインデックスパターンが含まれます。", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdHelpText": "手始めに使えるように、一般的なジョブがいくつか提供されています。独自のカスタムジョブを追加するには、{machineLearning} アプリケーションでジョブに「siem」のグループを割り当て、ここに表示されるようにします。", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdHelpText": "手始めに使えるように、一般的なジョブがいくつか提供されています。独自のカスタムジョブを追加するには、{machineLearning} アプリケーションでジョブに「security」のグループを割り当て、ここに表示されるようにします。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired": "機械学習ジョブが必要です。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.mlEnableJobWarningTitle": "このMLジョブは現在実行されていません。このルールを有効にする前に、このジョブを「MLジョブ設定」で実行するように設定してください。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.mlJobSelectPlaceholderText": "ジョブを選択してください", @@ -15655,6 +15459,7 @@ "xpack.securitySolution.detectionEngine.details.stepAboutRule.aboutText": "概要", "xpack.securitySolution.detectionEngine.details.stepAboutRule.detailsLabel": "詳細", "xpack.securitySolution.detectionEngine.details.stepAboutRule.investigationGuideLabel": "調査ガイド", + "xpack.securitySolution.detectionEngine.detectionsBreadcrumbTitle": "検出", "xpack.securitySolution.detectionEngine.detectionsPageTitle": "検出アラート", "xpack.securitySolution.detectionEngine.dismissButton": "閉じる", "xpack.securitySolution.detectionEngine.dismissNoApiIntegrationKeyButton": "閉じる", @@ -15664,6 +15469,7 @@ "xpack.securitySolution.detectionEngine.editRule.errorMsgDescription": "申し訳ありません", "xpack.securitySolution.detectionEngine.editRule.pageTitle": "ルール設定の編集", "xpack.securitySolution.detectionEngine.editRule.saveChangeTitle": "変更を保存", + "xpack.securitySolution.detectionEngine.emptyActionBeats": "セットアップの手順を表示", "xpack.securitySolution.detectionEngine.emptyActionSecondary": "ドキュメントに移動", "xpack.securitySolution.detectionEngine.emptyTitle": "セキュリティアプリケーションの検出エンジンに関連したインデックスがないようです", "xpack.securitySolution.detectionEngine.goToDocumentationButton": "ドキュメンテーションを表示", @@ -15955,6 +15761,10 @@ "xpack.securitySolution.detectionEngine.mitreAttackTechniques.xslScriptProcessingDescription": "XSLスクリプト処理(T1220)", "xpack.securitySolution.detectionEngine.mlRulesDisabledMessageTitle": "MLルールにはプラチナライセンスとML管理者権限が必要です", "xpack.securitySolution.detectionEngine.mlUnavailableTitle": "{totalRules} {totalRules, plural, =1 {個のルール} other {個のルール}}で機械学習を有効にする必要があります。", + "xpack.securitySolution.detectionEngine.needsIndexPermissionsMessage": "検出エンジンを使用するには、必要なクラスターおよびインデックス権限をもつユーザーが最初にこのページにアクセスする必要があります。{additionalContext}ヘルプについては、Elastic Stack管理者にお問い合わせください。", + "xpack.securitySolution.detectionEngine.needsListsIndexesMessage": "listsインデックスの権限が必要です。", + "xpack.securitySolution.detectionEngine.needsSignalsAndListsIndexesMessage": "signalsおよびlistsインデックスの権限が必要です。", + "xpack.securitySolution.detectionEngine.needsSignalsIndexMessage": "signalsインデックスの権限が必要です。", "xpack.securitySolution.detectionEngine.noApiIntegrationKeyCallOutMsg": "Kibanaを起動するごとに保存されたオブジェクトの新しい暗号化キーを作成します。永続キーがないと、Kibanaの再起動後にルールを削除または修正することができません。永続キーを設定するには、kibana.ymlファイルに32文字以上のテキスト値を付けてxpack.encryptedSavedObjects.encryptionKey設定を追加してください。", "xpack.securitySolution.detectionEngine.noApiIntegrationKeyCallOutTitle": "API統合キーが必要です", "xpack.securitySolution.detectionEngine.noIndexTitle": "検出エンジンを設定しましょう", @@ -16046,9 +15856,9 @@ "xpack.securitySolution.detectionEngine.rules.optionalFieldDescription": "オプション", "xpack.securitySolution.detectionEngine.rules.pageTitle": "検出ルール", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.createOwnRuletButton": "独自のルールの作成", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elasticセキュリティには、バックグラウンドで実行され、条件が合うとアラートを作成する事前構築済み検出ルールがあります。デフォルトでは、すべての事前構築済みルールが無効化されていて、有効化したいルールを選択します。", + "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elasticセキュリティには、バックグラウンドで実行され、条件が合うとアラートを作成する事前構築済み検出ルールがあります。デフォルトでは、Elastic Endpoint Securityルールを除くすべての事前構築済みルールが無効になっています。有効にする追加のルールを選択することができます。", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptTitle": "Elastic事前構築済み検出ルールを読み込む", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "事前構築済み検知ルールを読み込む", + "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "事前構築済み検出ルールおよびタイムラインテンプレートを読み込む", "xpack.securitySolution.detectionEngine.rules.releaseNotesHelp": "リリースノート", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton": "{missingRules} Elastic事前構築済み{missingRules, plural, =1 {ルール} other {ルール}}と{missingTimelines} Elastic事前構築済み{missingTimelines, plural, =1 {タイムライン} other {タイムライン}}をインストール ", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton": "{missingRules} Elasticの事前構築済みの{missingRules, plural, =1 {個のルール} other {個のルール}}をインストール ", @@ -16077,6 +15887,7 @@ "xpack.securitySolution.detectionEngine.totalSignalTitle": "合計", "xpack.securitySolution.detectionEngine.userUnauthenticatedMsgBody": "検出エンジンを表示するための必要なアクセス権がありません。ヘルプについては、管理者にお問い合わせください。", "xpack.securitySolution.detectionEngine.userUnauthenticatedTitle": "検出エンジンアクセス権が必要です", + "xpack.securitySolution.detectionEngine.validations.thresholdValueFieldData.numberGreaterThanOrEqualOneErrorMessage": "値は1以上でなければなりません。", "xpack.securitySolution.dragAndDrop.addToTimeline": "タイムライン調査に追加", "xpack.securitySolution.dragAndDrop.closeButtonLabel": "閉じる", "xpack.securitySolution.dragAndDrop.copyToClipboardTooltip": "クリップボードにコピー", @@ -16098,72 +15909,15 @@ "xpack.securitySolution.editDataProvider.selectAnOperatorPlaceholder": "演算子を選択", "xpack.securitySolution.editDataProvider.valueLabel": "値", "xpack.securitySolution.editDataProvider.valuePlaceholder": "値", - "xpack.securitySolution.emptyMessage": "Elastic セキュリティは無料かつオープンのElastic SIEMに、Elastic Endpointを搭載。脅威の防御と検知、脅威への対応を支援します。開始するには、セキュリティソリューション関連データをElastic Stackに追加する必要があります。詳細については、ご覧ください ", + "xpack.securitySolution.emptyMessage": "Elastic セキュリティは無料かつオープンのElastic SIEMに、Elastic Endpoint Securityを搭載。脅威の防御と検知、脅威への対応を支援します。開始するには、セキュリティソリューション関連データをElastic Stackに追加する必要があります。詳細については、以下をご覧ください ", "xpack.securitySolution.emptyString.emptyStringDescription": "空の文字列", - "xpack.securitySolution.endpoint.details.endpointVersion": "エンドポイントバージョン", - "xpack.securitySolution.endpoint.details.errorBody": "フライアウトを終了して、利用可能なホストを選択してください。", - "xpack.securitySolution.endpoint.details.errorTitle": "ホストが見つかりませんでした", - "xpack.securitySolution.endpoint.details.hostname": "ホスト名", - "xpack.securitySolution.endpoint.details.ipAddress": "IP アドレス", - "xpack.securitySolution.endpoint.details.lastSeen": "前回の認識", - "xpack.securitySolution.endpoint.details.linkToIngestTitle": "ポリシーの再割り当て", - "xpack.securitySolution.endpoint.details.os": "OS", - "xpack.securitySolution.endpoint.details.policy": "ポリシー", - "xpack.securitySolution.endpoint.details.policyStatus": "ポリシーステータス", - "xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失敗} other {不明}}", - "xpack.securitySolution.endpoint.policyResponse.backLinkTitle": "エンドポイント詳細", - "xpack.securitySolution.endpoint.policyResponse.title": "ポリシー応答", - "xpack.securitySolution.endpoint.details.noPolicyResponse": "ポリシー応答がありません", - "xpack.securitySolution.endpoint.details.policyResponse.configure_dns_events": "DNSイベントの構成", - "xpack.securitySolution.endpoint.details.policyResponse.configure_elasticsearch_connection": "Elastic Search接続の構成", - "xpack.securitySolution.endpoint.details.policyResponse.configure_file_events": "ファイルイベントの構成", - "xpack.securitySolution.endpoint.details.policyResponse.configure_imageload_events": "画像読み込みイベントの構成", - "xpack.securitySolution.endpoint.details.policyResponse.configure_kernel": "カーネルの構成", - "xpack.securitySolution.endpoint.details.policyResponse.configure_logging": "ロギングの構成", - "xpack.securitySolution.endpoint.details.policyResponse.configure_malware": "マルウェアの構成", - "xpack.securitySolution.endpoint.details.policyResponse.configure_network_events": "ネットワークイベントの構成", - "xpack.securitySolution.endpoint.details.policyResponse.configure_process_events": "プロセスイベントの構成", - "xpack.securitySolution.endpoint.details.policyResponse.configure_registry_events": "レジストリイベントの構成", - "xpack.securitySolution.endpoint.details.policyResponse.configure_security_events": "セキュリティイベントの構成", - "xpack.securitySolution.endpoint.details.policyResponse.connect_kernel": "カーネルを接続", - "xpack.securitySolution.endpoint.details.policyResponse.detect_async_image_load_events": "非同期画像読み込みイベントを検出", - "xpack.securitySolution.endpoint.details.policyResponse.detect_file_open_events": "ファイルオープンイベントを検出", - "xpack.securitySolution.endpoint.details.policyResponse.detect_file_write_events": "ファイル書き込みイベントを検出", - "xpack.securitySolution.endpoint.details.policyResponse.detect_network_events": "ネットワークイベントを検出", - "xpack.securitySolution.endpoint.details.policyResponse.detect_process_events": "プロセスイベントを検出", - "xpack.securitySolution.endpoint.details.policyResponse.detect_registry_events": "レジストリイベントを検出", - "xpack.securitySolution.endpoint.details.policyResponse.detect_sync_image_load_events": "同期画像読み込みイベントを検出", - "xpack.securitySolution.endpoint.details.policyResponse.download_global_artifacts": "グローバルアーチファクトをダウンロード", - "xpack.securitySolution.endpoint.details.policyResponse.download_user_artifacts": "ユーザーアーチファクトをダウンロード", - "xpack.securitySolution.endpoint.details.policyResponse.events": "イベント", - "xpack.securitySolution.endpoint.details.policyResponse.failed": "失敗", - "xpack.securitySolution.endpoint.details.policyResponse.load_config": "構成の読み込み", - "xpack.securitySolution.endpoint.details.policyResponse.load_malware_model": "マルウェアモデルの読み込み", - "xpack.securitySolution.endpoint.details.policyResponse.logging": "ログ", - "xpack.securitySolution.endpoint.details.policyResponse.malware": "マルウェア", - "xpack.securitySolution.endpoint.details.policyResponse.read_elasticsearch_config": "Elasticsearch構成を読み取る", - "xpack.securitySolution.endpoint.details.policyResponse.read_events_config": "イベント構成を読み取る", - "xpack.securitySolution.endpoint.details.policyResponse.read_kernel_config": "カーネル構成を読み取る", - "xpack.securitySolution.endpoint.details.policyResponse.read_logging_config": "ロギング構成を読み取る", - "xpack.securitySolution.endpoint.details.policyResponse.read_malware_config": "マルウェア構成を読み取る", - "xpack.securitySolution.endpoint.details.policyResponse.streaming": "ストリーム中…", - "xpack.securitySolution.endpoint.details.policyResponse.success": "成功", - "xpack.securitySolution.endpoint.details.policyResponse.warning": "警告", - "xpack.securitySolution.endpoint.details.policyResponse.workflow": "ワークフロー", - "xpack.securitySolution.endpoint.list.loadingPolicies": "ポリシー構成を読み込んでいます…", - "xpack.securitySolution.endpoint.list.noEndpointsInstructions": "セキュリティポリシーを作成しました。以下のステップに従い、エージェントでElastic Endpoint Security機能を有効にする必要があります。", - "xpack.securitySolution.endpoint.list.noEndpointsPrompt": "エージェントでElastic Endpoint Securityを有効にする", - "xpack.securitySolution.endpoint.list.noPolicies": "ポリシーがありません。", - "xpack.securitySolution.endpoint.list.stepOne": "既存のポリシーは以下のリストのとおりです。これは後から変更できます。", - "xpack.securitySolution.endpoint.list.stepOneTitle": "ホストの保護で使用するポリシーを選択", - "xpack.securitySolution.endpoint.list.stepTwo": "開始するために必要なコマンドが提供されます。", - "xpack.securitySolution.endpoint.list.stepTwoTitle": "Ingest Manager経由でEndpoint Securityによって有効にされたエージェントを登録", - "xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration": "このエージェント構成を使用するすべてのエージェントは基本ポリシーを使用します。セキュリティアプリでこのポリシーを変更できます。Fleetはこれらの変更をエージェントにデプロイします。", "xpack.securitySolution.endpoint.ingestToastMessage": "Ingest Managerが設定中に失敗しました。", "xpack.securitySolution.endpoint.ingestToastTitle": "アプリを初期化できませんでした", - "xpack.securitySolution.endpoint.policy.details.backToListTitle": "ポリシーリストに戻る", + "xpack.securitySolution.endpoint.policy.details.backToListTitle": "エンドポイントホストに戻る", "xpack.securitySolution.endpoint.policy.details.cancel": "キャンセル", "xpack.securitySolution.endpoint.policy.details.detect": "検知", + "xpack.securitySolution.endpoint.policy.details.detectionRulesLink": "関連する検出ルール", + "xpack.securitySolution.endpoint.policy.details.detectionRulesMessage": "{detectionRulesLink}を表示します。事前構築済みルールは、[検出ルール]ページで「Elastic」というタグが付けられています。", "xpack.securitySolution.endpoint.policy.details.eventCollection": "イベント収集", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total}件のイベント収集が有効です", "xpack.securitySolution.endpoint.policy.details.linux": "Linux", @@ -16178,9 +15932,9 @@ "xpack.securitySolution.endpoint.policy.details.updateConfirm.confirmButtonTitle": "変更を保存してデプロイ", "xpack.securitySolution.endpoint.policy.details.updateConfirm.message": "この操作は元に戻すことができません。続行していいですか?", "xpack.securitySolution.endpoint.policy.details.updateConfirm.title": "変更を保存してデプロイ", - "xpack.securitySolution.endpoint.policy.details.updateConfirm.warningMessage": "これらの変更を保存すると、このポリシーに割り当てられたすべての有効なエンドポイントに更新が適用されます。", + "xpack.securitySolution.endpoint.policy.details.updateConfirm.warningMessage": "これらの変更を保存すると、このエージェント構成に割り当てられたすべての有効なエンドポイントに更新が適用されます。", "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失敗しました。", - "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "ポリシー{name}が更新されました。", + "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "統合{name}が更新されました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", "xpack.securitySolution.endpoint.policy.details.windows": "Windows", "xpack.securitySolution.endpoint.policy.details.windowsAndMac": "Windows、Mac", @@ -16207,7 +15961,6 @@ "xpack.securitySolution.endpoint.policyDetailType": "タイプ", "xpack.securitySolution.endpoint.policyList.actionButtonText": "Endpoint Securityを追加", "xpack.securitySolution.endpoint.policyList.actionMenu": "開く", - "xpack.securitySolution.endpoint.policyList.agentPolicyAction": "エージェント構成を表示", "xpack.securitySolution.endpoint.policyList.createdAt": "作成日時", "xpack.securitySolution.endpoint.policyList.createdBy": "作成者", "xpack.securitySolution.endpoint.policyList.createNewButton": "新しいポリシーを作成", @@ -16233,6 +15986,7 @@ "xpack.securitySolution.endpoint.policyList.revision": "rev. {revNumber}", "xpack.securitySolution.endpoint.policyList.updatedAt": "最終更新", "xpack.securitySolution.endpoint.policyList.updatedBy": "最終更新者", + "xpack.securitySolution.endpoint.policyList.versionField": "v{version}", "xpack.securitySolution.endpoint.policyList.versionFieldLabel": "バージョン", "xpack.securitySolution.endpoint.policyList.viewTitleTotalCount": "{totalItemCount, plural, one {# ポリシー} other {# ポリシー}}", "xpack.securitySolution.endpoint.resolver.eitherLineageLimitExceeded": "以下のビジュアライゼーションとイベントリストの一部のプロセスイベントを表示できませんでした。データの上限に達しました。", @@ -16246,7 +16000,6 @@ "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.eventDescriptiveName": "{descriptor} {subject}", "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events": "イベント", "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.wait": "イベントを待機しています...", - "xpack.securitySolution.resolver.panel.nodeList.title": "すべてのプロセスイベント", "xpack.securitySolution.endpoint.resolver.panel.relatedCounts.numberOfEventsInCrumb": "{totalCount}件のイベント", "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing": "関連イベントが見つかりません。", "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait": "イベントを待機しています...", @@ -16277,16 +16030,6 @@ "xpack.securitySolution.endpoint.resolver.runningTrigger": "トリガーの実行中", "xpack.securitySolution.endpoint.resolver.terminatedProcess": "プロセスを中断しました", "xpack.securitySolution.endpoint.resolver.terminatedTrigger": "トリガーを中断しました", - "xpack.securitySolution.endpoint.list.endpointVersion": "バージョン", - "xpack.securitySolution.endpoint.list.hostname": "ホスト名", - "xpack.securitySolution.endpoint.list.hostStatus": "ホストステータス", - "xpack.securitySolution.endpoint.list.hostStatusValue": "{hostStatus, select, online {オンライン} error {エラー} other {オフライン}}", - "xpack.securitySolution.endpoint.list.ip": "IP アドレス", - "xpack.securitySolution.endpoint.list.lastActive": "前回のアーカイブ", - "xpack.securitySolution.endpoint.list.os": "オペレーティングシステム", - "xpack.securitySolution.endpoint.list.policy": "ポリシー", - "xpack.securitySolution.endpoint.list.policyStatus": "ポリシーステータス", - "xpack.securitySolution.endpoint.list.totalCount": "{totalItemCount, plural, one {# ホスト} other {# ホスト}}", "xpack.securitySolution.endpointManagement.noPermissionsSubText": "Ingest Managerが無効である可能性があります。この機能を使用するには、Ingest Managerを有効にする必要があります。Ingest Managerを有効にする権限がない場合は、Kibana管理者に連絡してください。", "xpack.securitySolution.endpointManagemnet.noPermissionsText": "Elastic Security Administrationを使用するために必要なKibana権限がありません。", "xpack.securitySolution.enpdoint.resolver.panelutils.betaBadgeLabel": "BETA", @@ -16344,33 +16087,48 @@ "xpack.securitySolution.eventsViewer.footer.loadingEventsDataLabel": "イベントを読み込み中", "xpack.securitySolution.eventsViewer.showingLabel": "表示中", "xpack.securitySolution.eventsViewer.unit": "{totalCount, plural, =1 {イベント} other {イベント}}", + "xpack.securitySolution.exceptions.addException.addEndpointException": "エンドポイント例外の追加", "xpack.securitySolution.exceptions.addException.addException": "例外の追加", - "xpack.securitySolution.exceptions.addException.bulkCloseLabel": "この例外の属性と一致するすべてのアラートを閉じる", + "xpack.securitySolution.exceptions.addException.bulkCloseLabel": "他のルールで生成されたアラートを含む、この例外と一致するすべてのアラートを終了", "xpack.securitySolution.exceptions.addException.bulkCloseLabel.disabled": "この例外の属性と一致するすべてのアラートを閉じる(リストと非ECSフィールドはサポートされません)", "xpack.securitySolution.exceptions.addException.cancel": "キャンセル", - "xpack.securitySolution.exceptions.addException.endpointQuarantineText": "選択した属性と一致する任意のエンドポイントの隔離にあるファイルは、自動的に元の場所に復元されます。", + "xpack.securitySolution.exceptions.addException.endpointQuarantineText": "すべてのエンドポイントホストで、例外と一致する隔離されたファイルは、自動的に元の場所に復元されます。この例外はエンドポイント例外を使用するすべてのルールに適用されます。", "xpack.securitySolution.exceptions.addException.error": "例外を追加できませんでした", "xpack.securitySolution.exceptions.addException.fetchError": "例外リストの取得エラー", "xpack.securitySolution.exceptions.addException.fetchError.title": "エラー", "xpack.securitySolution.exceptions.addException.infoLabel": "ルールの条件が満たされるときにアラートが生成されます。例外:", "xpack.securitySolution.exceptions.addException.success": "正常に例外を追加しました", "xpack.securitySolution.exceptions.andDescription": "AND", + "xpack.securitySolution.exceptions.builder.addNestedDescription": "ネストされた条件を追加", + "xpack.securitySolution.exceptions.builder.addNonNestedDescription": "ネストされていない条件を追加", + "xpack.securitySolution.exceptions.builder.exceptionFieldNestedPlaceholderDescription": "ネストされたフィールドを検索", + "xpack.securitySolution.exceptions.builder.exceptionFieldPlaceholderDescription": "検索", + "xpack.securitySolution.exceptions.builder.exceptionFieldValuePlaceholderDescription": "検索フィールド値...", + "xpack.securitySolution.exceptions.builder.exceptionListsPlaceholderDescription": "リストを検索...", + "xpack.securitySolution.exceptions.builder.exceptionOperatorPlaceholderDescription": "演算子", + "xpack.securitySolution.exceptions.builder.fieldDescription": "フィールド", + "xpack.securitySolution.exceptions.builder.operatorDescription": "演算子", + "xpack.securitySolution.exceptions.builder.valueDescription": "値", "xpack.securitySolution.exceptions.commentEventLabel": "コメントを追加しました", "xpack.securitySolution.exceptions.commentLabel": "コメント", "xpack.securitySolution.exceptions.createdByLabel": "作成者", "xpack.securitySolution.exceptions.dateCreatedLabel": "日付が作成されました", + "xpack.securitySolution.exceptions.descriptionLabel": "説明", "xpack.securitySolution.exceptions.detectionListLabel": "検出リスト", "xpack.securitySolution.exceptions.doesNotExistOperatorLabel": "存在しない", "xpack.securitySolution.exceptions.editButtonLabel": "編集", - "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "この例外の属性と一致するすべてのアラートを閉じる", + "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "他のルールで生成されたアラートを含む、この例外と一致するすべてのアラートを終了", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "この例外の属性と一致するすべてのアラートを閉じる(リストと非ECSフィールドはサポートされません)", "xpack.securitySolution.exceptions.editException.cancel": "キャンセル", + "xpack.securitySolution.exceptions.editException.editEndpointExceptionTitle": "エンドポイント例外の編集", "xpack.securitySolution.exceptions.editException.editExceptionSaveButton": "保存", "xpack.securitySolution.exceptions.editException.editExceptionTitle": "例外の編集", - "xpack.securitySolution.exceptions.editException.endpointQuarantineText": "選択した属性と一致する任意のエンドポイントの隔離にあるファイルは、自動的に元の場所に復元されます。", + "xpack.securitySolution.exceptions.editException.endpointQuarantineText": "すべてのエンドポイントホストで、例外と一致する隔離されたファイルは、自動的に元の場所に復元されます。この例外はエンドポイント例外を使用するすべてのルールに適用されます。", "xpack.securitySolution.exceptions.editException.error": "例外を更新できませんでした", "xpack.securitySolution.exceptions.editException.infoLabel": "ルールの条件が満たされるときにアラートが生成されます。例外:", "xpack.securitySolution.exceptions.editException.success": "正常に例外を更新しました", + "xpack.securitySolution.exceptions.editException.versionConflictDescription": "最初に編集することを選択したときからこの例外が更新されている可能性があります。[キャンセル]をクリックし、もう一度例外を編集してください。", + "xpack.securitySolution.exceptions.editException.versionConflictTitle": "申し訳ございません、エラーが発生しました", "xpack.securitySolution.exceptions.endpointListLabel": "エンドポイントリスト", "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "ページごとの項目数: {items}", "xpack.securitySolution.exceptions.existsOperatorLabel": "存在する", @@ -16393,9 +16151,9 @@ "xpack.securitySolution.exceptions.valueDescription": "値", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "新しいコメントを追加...", "xpack.securitySolution.exceptions.viewer.addExceptionLabel": "新しい例外を追加", - "xpack.securitySolution.exceptions.viewer.addToClipboard": "クリップボードに追加", - "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "検出リストに追加", - "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "エンドポイントリストに追加", + "xpack.securitySolution.exceptions.viewer.addToClipboard": "コメント", + "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "ルール例外の追加", + "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "エンドポイント例外の追加", "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "例外の削除エラー", "xpack.securitySolution.exceptions.viewer.emptyPromptBody": "例外を追加してルールを微調整し、例外条件が満たされたときに検出アラートが作成されないようにすることができます。例外により検出の精度が改善します。これにより、誤検出数が減ります。", "xpack.securitySolution.exceptions.viewer.emptyPromptTitle": "このルールには例外がありません", @@ -16404,6 +16162,8 @@ "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription": "このルールのすべての例外は、エンドポイントと検出ルールに適用されます。詳細については、{ruleSettings}を確認してください。", "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription.ruleSettingsLink": "ルール設定", "xpack.securitySolution.exceptions.viewer.fetchingListError": "例外の取得エラー", + "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "例外項目合計数の取得エラー", + "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "検索結果が見つかりません。", "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "検索フィールド(例:host.name)", "xpack.securitySolution.exitFullScreenButton": "全画面を終了", "xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle": "セキュリティ", @@ -16452,11 +16212,12 @@ "xpack.securitySolution.header.editableTitle.editButtonAria": "クリックすると {title} を編集できます", "xpack.securitySolution.header.editableTitle.save": "保存", "xpack.securitySolution.headerGlobal.buttonAddData": "データの追加", + "xpack.securitySolution.headerGlobal.securitySolution": "セキュリティソリューション", "xpack.securitySolution.headerPage.pageSubtitle": "前回のイベント: {beat}", "xpack.securitySolution.hooks.useAddToTimeline.addedFieldMessage": "{fieldOrValue}をタイムラインに追加しました", "xpack.securitySolution.host.details.architectureLabel": "アーキテクチャー", - "xpack.securitySolution.host.details.endpoint.endpointPolicy": "エンドポイントポリシー", - "xpack.securitySolution.host.details.endpoint.policyStatus": "ポリシーステータス", + "xpack.securitySolution.host.details.endpoint.endpointPolicy": "統合", + "xpack.securitySolution.host.details.endpoint.policyStatus": "構成ステータス", "xpack.securitySolution.host.details.endpoint.sensorversion": "センサーバージョン", "xpack.securitySolution.host.details.firstSeenTitle": "初回の認識", "xpack.securitySolution.host.details.lastSeenTitle": "前回の認識", @@ -16473,8 +16234,6 @@ "xpack.securitySolution.host.details.overview.platformTitle": "プラットフォーム", "xpack.securitySolution.host.details.overview.regionTitle": "地域", "xpack.securitySolution.host.details.versionLabel": "バージョン", - "xpack.securitySolution.endpoint.list.pageSubTitle": "Elastic Endpoint Securityを実行しているホスト", - "xpack.securitySolution.endpoint.list.pageTitle": "ホスト", "xpack.securitySolution.hosts.kqlPlaceholder": "例:host.name: \"foo\"", "xpack.securitySolution.hosts.navigation.alertsTitle": "外部アラート", "xpack.securitySolution.hosts.navigation.allHostsTitle": "すべてのホスト", @@ -16486,7 +16245,6 @@ "xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingAuthenticationsData": "認証データをクエリできませんでした", "xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingEventsData": "イベントデータをクエリできませんでした", "xpack.securitySolution.hosts.pageTitle": "ホスト", - "xpack.securitySolution.endpointsTab": "ホスト", "xpack.securitySolution.hostsTable.firstLastSeenToolTip": "選択された日付範囲との相関付けです", "xpack.securitySolution.hostsTable.hostsTitle": "すべてのホスト", "xpack.securitySolution.hostsTable.lastSeenTitle": "前回の認識", @@ -16531,12 +16289,17 @@ "xpack.securitySolution.lists.cancelValueListsUploadTitle": "アップロードのキャンセル", "xpack.securitySolution.lists.closeValueListsModalTitle": "閉じる", "xpack.securitySolution.lists.detectionEngine.rules.uploadValueListsButton": "値リストのアップロード", - "xpack.securitySolution.lists.uploadValueListDescription": "ルールまたはルール例外の書き込み中に使用する単一値リストをアップロードします。", + "xpack.securitySolution.lists.detectionEngine.rules.uploadValueListsButtonTooltip": "値リストを使用して、フィールド値がリストの値と一致したときに例外を作成します", + "xpack.securitySolution.lists.uploadValueListDescription": "ルール例外の書き込み中に使用する単一値リストをアップロードします。", + "xpack.securitySolution.lists.uploadValueListExtensionValidationMessage": "ファイルは次の種類のいずれかでなければなりません:[{fileTypes}]", "xpack.securitySolution.lists.uploadValueListPrompt": "ファイルを選択するかドラッグ &amp; ドロップしてください", "xpack.securitySolution.lists.uploadValueListTitle": "値リストのアップロード", + "xpack.securitySolution.lists.valueListsExportError": "値リストのエクスポート中にエラーが発生しました。", "xpack.securitySolution.lists.valueListsForm.ipRadioLabel": "IP アドレス", + "xpack.securitySolution.lists.valueListsForm.ipRangesRadioLabel": "IP 範囲", "xpack.securitySolution.lists.valueListsForm.keywordsRadioLabel": "キーワード", "xpack.securitySolution.lists.valueListsForm.listTypesRadioLabel": "値リストのタイプ", + "xpack.securitySolution.lists.valueListsForm.textRadioLabel": "テキスト", "xpack.securitySolution.lists.valueListsTable.actionsColumn": "アクション", "xpack.securitySolution.lists.valueListsTable.createdByColumn": "作成者", "xpack.securitySolution.lists.valueListsTable.deleteActionDescription": "値リストの削除", @@ -16745,8 +16508,8 @@ "xpack.securitySolution.overview.endpointNotice.dismiss": "メッセージを消去", "xpack.securitySolution.overview.endpointNotice.introducing": "導入: ", "xpack.securitySolution.overview.endpointNotice.message": "脅威防御、検出、深いセキュリティデータの可視化を実現し、ホストを保護します。", - "xpack.securitySolution.overview.endpointNotice.title": "Elastic Endpoint Securityベータ", - "xpack.securitySolution.overview.endpointNotice.tryButton": "Elastic Endpoint Securityベータを試す", + "xpack.securitySolution.overview.endpointNotice.title": "Elastic Endpoint Security(ベータ)", + "xpack.securitySolution.overview.endpointNotice.tryButton": "Elastic Endpoint Security(ベータ)を試す", "xpack.securitySolution.overview.eventsTitle": "イベント数", "xpack.securitySolution.overview.feedbackText": "Elastic SIEM に関するご意見やご提案は、お気軽に {feedback}", "xpack.securitySolution.overview.feedbackText.feedbackLinkText": "フィードバックをオンラインで送信", @@ -16792,9 +16555,14 @@ "xpack.securitySolution.overview.viewEventsButtonLabel": "イベントを表示", "xpack.securitySolution.overview.winlogbeatMWSysmonOperational": "Microsoft-Windows-Sysmon/Operational", "xpack.securitySolution.overview.winlogbeatSecurityTitle": "セキュリティ", - "xpack.securitySolution.pages.common.emptyActionEndpoint": "Elasticエージェント(ベータ)でデータを追加", + "xpack.securitySolution.pages.common.emptyActionBeats": "Beatsでデータを追加", + "xpack.securitySolution.pages.common.emptyActionBeatsDescription": "Lightweight Beatsは数百または数千台のコンピューターとシステムからデータを送信できます", + "xpack.securitySolution.pages.common.emptyActionElasticAgent": "Elasticエージェントでデータを追加", + "xpack.securitySolution.pages.common.emptyActionElasticAgentDescription": "Elasticエージェントでは、シンプルかつ統合された方法で、監視をホストに追加することができます。", + "xpack.securitySolution.pages.common.emptyActionEndpoint": "Elastic Endpoint Securityを追加", + "xpack.securitySolution.pages.common.emptyActionEndpointDescription": "脅威防御、検出、深いセキュリティデータの可視化を実現し、ホストを保護します。", "xpack.securitySolution.pages.common.emptyActionSecondary": "入門ガイドを表示します。", - "xpack.securitySolution.pages.common.emptyTitle": "セキュリティソリューションへようこそ。始めましょう。", + "xpack.securitySolution.pages.common.emptyTitle": "Elastic Securityへようこそ。始めましょう。", "xpack.securitySolution.pages.fourohfour.noContentFoundDescription": "コンテンツがありません", "xpack.securitySolution.paginatedTable.rowsButtonLabel": "ページごとの行数", "xpack.securitySolution.paginatedTable.showingSubtitle": "表示中", @@ -16898,7 +16666,7 @@ "xpack.securitySolution.timeline.body.renderers.endgame.withSpecialPrivilegesDescription": "割り当てられた特別な権限", "xpack.securitySolution.timeline.body.sort.sortedAscendingTooltip": "昇順で並べ替えます", "xpack.securitySolution.timeline.body.sort.sortedDescendingTooltip": "降順で並べ替えます", - "xpack.securitySolution.timeline.callOut.immutable.message.description": "タイムラインを引き続き使用して、セキュリティイベントの検索とフィルターはできますが、このタイムラインは変わらないため、セキュリティアプリケーションで保存することはできません。", + "xpack.securitySolution.timeline.callOut.immutable.message.description": "この事前構築済みタイムラインテンプレートを修正することはできません。変更するには、このテンプレートを複製し、複製したテンプレートを修正します。", "xpack.securitySolution.timeline.callOut.unauthorized.message.description": "タイムラインを使用すると、イベントを調査することができますが、今後使用する目的でタイムラインを保存するために必要な権限がありません。タイムラインを保存する必要がある場合は、Kibana管理者に連絡してください。", "xpack.securitySolution.timeline.categoryTooltip": "カテゴリー", "xpack.securitySolution.timeline.defaultTimelineDescription": "新しいタイムラインを作成するときにデフォルトで提供されるタイムライン。", @@ -16920,6 +16688,7 @@ "xpack.securitySolution.timeline.flyoutTimelineTemplateLabel": "タイムラインテンプレート", "xpack.securitySolution.timeline.fullScreenButton": "全画面", "xpack.securitySolution.timeline.graphOverlay.backToEventsButton": "< イベントに戻る", + "xpack.securitySolution.timeline.properties.attachTimelineToCaseTooltip": "ケースに関連付けるには、タイムラインのタイトルを入力してください", "xpack.securitySolution.timeline.properties.attachToExistingCaseButtonLabel": "既存のケースに添付...", "xpack.securitySolution.timeline.properties.attachToNewCaseButtonLabel": "新しいケースに添付", "xpack.securitySolution.timeline.properties.descriptionPlaceholder": "説明", @@ -17829,8 +17598,13 @@ "xpack.spaces.management.confirmDeleteModal.deletingSpaceWarningMessage": "スペースを削除すると、スペースと {allContents} が永久に削除されます。この操作は元に戻すことができません。", "xpack.spaces.management.confirmDeleteModal.redirectAfterDeletingCurrentSpaceWarningMessage": "現在のスペース {name} を削除しようとしています。続行すると、別のスペースを選択する画面に移動します。", "xpack.spaces.management.confirmDeleteModal.spaceNamesDoNoMatchErrorMessage": "スペース名が一致していません。", + "xpack.spaces.management.copyToSpace.actionDescription": "この保存されたオブジェクトを1つまたは複数のスペースにコピーします。", + "xpack.spaces.management.copyToSpace.actionTitle": "スペースにコピー", "xpack.spaces.management.copyToSpace.copyErrorTitle": "保存されたオブジェクトのコピー中にエラーが発生", + "xpack.spaces.management.copyToSpace.copyResultsLabel": "コピー結果", "xpack.spaces.management.copyToSpace.copyStatus.conflictsMessage": "このスペースには同じID({id})の保存されたオブジェクトが既に存在します。", + "xpack.spaces.management.copyToSpace.copyStatus.conflictsOverwriteMessage": "「上書き」をクリックしてこのバージョンをコピーされたバージョンに置き換えます。", + "xpack.spaces.management.copyToSpace.copyStatus.pendingOverwriteMessage": "保存されたオブジェクトは上書きされます。「スキップ」をクリックしてこの操作をキャンセルします。", "xpack.spaces.management.copyToSpace.copyStatus.successMessage": "保存されたオブジェクトがコピーされました。", "xpack.spaces.management.copyToSpace.copyStatus.unresolvableErrorMessage": "この保存されたオブジェクトのコピー中にエラーが発生しました。", "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "{space}スペースに1つまたは複数の矛盾が検出されました。解決するにはこのセクションを拡張してください。", @@ -17838,17 +17612,24 @@ "xpack.spaces.management.copyToSpace.copyStatusSummary.successMessage": "{space}スペースにコピーされました。", "xpack.spaces.management.copyToSpace.copyToSpacesButton": "{spaceCount} {spaceCount, plural, one {スペース} other {スペース}}にコピー", "xpack.spaces.management.copyToSpace.disabledCopyToSpacesButton": "コピー", + "xpack.spaces.management.copyToSpace.dontIncludeRelatedLabel": "関連性のある保存されたオブジェクトを含みません", + "xpack.spaces.management.copyToSpace.dontOverwriteLabel": "保存されたオブジェクトを上書きしません", "xpack.spaces.management.copyToSpace.finishCopyToSpacesButton": "終了", "xpack.spaces.management.copyToSpace.finishedButtonLabel": "コピーが完了しました。", + "xpack.spaces.management.copyToSpace.finishPendingOverwritesCopyToSpacesButton": "{overwriteCount}件のオブジェクトを上書き", + "xpack.spaces.management.copyToSpace.includeRelatedLabel": "関連性のある保存されたオブジェクトを含みます", "xpack.spaces.management.copyToSpace.inProgressButtonLabel": "コピーが進行中です。お待ちください。", "xpack.spaces.management.copyToSpace.noSpacesBody": "コピーできるスペースがありません。", "xpack.spaces.management.copyToSpace.noSpacesTitle": "スペースがありません", "xpack.spaces.management.copyToSpace.overwriteLabel": "保存されたオブジェクトを自動的に上書きしています", "xpack.spaces.management.copyToSpace.resolveCopyErrorTitle": "保存されたオブジェクトの矛盾の解決中にエラーが発生", + "xpack.spaces.management.copyToSpace.resolveCopySuccessTitle": "上書き成功", + "xpack.spaces.management.copyToSpace.selectSpacesLabel": "コピー先のスペースを選択してください", "xpack.spaces.management.copyToSpace.spacesLoadErrorTitle": "利用可能なスペースを読み込み中にエラーが発生", "xpack.spaces.management.copyToSpaceFlyoutFooter.errorCount": "エラー", "xpack.spaces.management.copyToSpaceFlyoutFooter.pendingCount": "保留中", "xpack.spaces.management.copyToSpaceFlyoutFooter.successCount": "コピー完了", + "xpack.spaces.management.copyToSpaceFlyoutHeader": "保存されたオブジェクトのスペースへのコピー", "xpack.spaces.management.createSpaceBreadcrumb": "作成", "xpack.spaces.management.customizeSpaceAvatar.colorFormRowLabel": "色", "xpack.spaces.management.customizeSpaceAvatar.imageUrl": "カスタム画像", @@ -18328,6 +18109,7 @@ "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.titleFieldLabel": "短い説明", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.urgencySelectFieldLabel": "緊急", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel": "ユーザー名", + "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowAction.apiUrlHelpLabel": "Personal Developer Instance for ServiceNowの構成", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "Slack に送信", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText": "Web フック URL が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel": "メッセージ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index cfee565a1da93..bd30703dd5bd6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -501,47 +501,6 @@ "core.ui.securityNavList.label": "安全", "core.ui.welcomeErrorMessage": "Elastic 未正确加载。检查服务器输出以了解详情。", "core.ui.welcomeMessage": "正在加载 Elastic", - "core.ui_settings.params.darkModeText": "为 Kibana UI 启用深色模式需要刷新页面,才能应用设置。", - "core.ui_settings.params.darkModeTitle": "深色模式", - "core.ui_settings.params.dateFormat.dayOfWeekText": "一周从哪一日开始?", - "core.ui_settings.params.dateFormat.dayOfWeekTitle": "周内日", - "core.ui_settings.params.dateFormat.optionsLinkText": "格式", - "core.ui_settings.params.dateFormat.scaled.intervalsLinkText": "ISO8601 时间间隔", - "core.ui_settings.params.dateFormat.scaledText": "定义在基于时间的数据按顺序呈现且格式化时间戳应适应度量时间间隔时所用格式的值。键是 {intervalsLink}。", - "core.ui_settings.params.dateFormat.scaledTitle": "缩放的日期格式", - "core.ui_settings.params.dateFormat.timezoneText": "应使用哪个时区。{defaultOption} 将使用您的浏览器检测到的时区。", - "core.ui_settings.params.dateFormat.timezoneTitle": "用于设置日期格式的时区", - "core.ui_settings.params.dateFormatText": "显示格式正确的日期时,请使用此{formatLink}", - "core.ui_settings.params.dateFormatTitle": "日期格式", - "core.ui_settings.params.dateNanosFormatText": "用于 Elasticsearch 的 {dateNanosLink} 数据类型", - "core.ui_settings.params.dateNanosFormatTitle": "纳秒格式的日期", - "core.ui_settings.params.dateNanosLinkTitle": "date_nanos", - "core.ui_settings.params.defaultRoute.defaultRouteIsRelativeValidationMessage": "必须是相对 URL。", - "core.ui_settings.params.defaultRoute.defaultRouteText": "此设置指定打开 Kibana 时的默认路由。您可以使用此设置修改打开 Kibana 时的登陆页面。路由必须是相对 URL。", - "core.ui_settings.params.defaultRoute.defaultRouteTitle": "默认路由", - "core.ui_settings.params.disableAnimationsText": "在 Kibana UI 中关闭所有没有必要的动画。刷新页面以应用更改。", - "core.ui_settings.params.disableAnimationsTitle": "禁用动画", - "core.ui_settings.params.maxCellHeightText": "表中单元格应占用的最大高度。设置为 0 可禁用截短", - "core.ui_settings.params.maxCellHeightTitle": "最大表单元格高度", - "core.ui_settings.params.notifications.banner.markdownLinkText": "Markdown 受支持", - "core.ui_settings.params.notifications.bannerLifetimeText": "在屏幕上显示横幅通知的时间(毫秒)。设置为 {infinityValue} 将禁用倒计时。", - "core.ui_settings.params.notifications.bannerLifetimeTitle": "横幅通知生存时间", - "core.ui_settings.params.notifications.bannerText": "用于向所有用户发送临时通知的定制横幅。{markdownLink}", - "core.ui_settings.params.notifications.bannerTitle": "定制横幅通知", - "core.ui_settings.params.notifications.errorLifetimeText": "在屏幕上显示错误通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", - "core.ui_settings.params.notifications.errorLifetimeTitle": "错误通知生存时间", - "core.ui_settings.params.notifications.infoLifetimeText": "在屏幕上显示信息通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", - "core.ui_settings.params.notifications.infoLifetimeTitle": "信息通知生存时间", - "core.ui_settings.params.notifications.warningLifetimeText": "在屏幕上显示警告通知的时间(毫秒)。设置为 {infinityValue} 将禁用。", - "core.ui_settings.params.notifications.warningLifetimeTitle": "警告通知生存时间", - "core.ui_settings.params.pageNavigationDesc": "更改导航样式", - "core.ui_settings.params.pageNavigationLegacy": "旧版", - "core.ui_settings.params.pageNavigationModern": "现代", - "core.ui_settings.params.pageNavigationName": "侧边导航样式", - "core.ui_settings.params.themeVersionText": "在用于 Kibana 当前和下一版本的主题间切换。需要刷新页面,才能应用设置。", - "core.ui_settings.params.themeVersionTitle": "主题版本", - "core.ui_settings.params.storeUrlText": "URL 有时会变得过长,以使得某些浏览器无法处理。为此,我们正在测试将 URL 的各个组成部分存储在会话存储中是否会有帮助。请告知我们这样做的效果!", - "core.ui_settings.params.storeUrlTitle": "将 URL 存储在会话存储中", "dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化", "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全屏", "dashboard.addExistingVisualizationLinkText": "将现有", @@ -714,8 +673,6 @@ "data.advancedSettings.timepicker.refreshIntervalDefaultsText": "时间筛选的默认刷新时间间隔。需要使用毫秒单位指定“值”。", "data.advancedSettings.timepicker.refreshIntervalDefaultsTitle": "时间筛选刷新时间间隔", "data.advancedSettings.timepicker.thisWeek": "本周", - "data.advancedSettings.timepicker.timeDefaultsText": "未使用时间筛选启动 Kibana 时要使用的时间筛选选择", - "data.advancedSettings.timepicker.timeDefaultsTitle": "时间筛选默认值", "data.advancedSettings.timepicker.today": "今日", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 和 {lt} {to}", "data.common.kql.errors.endOfInputText": "输入结束", @@ -1647,7 +1604,6 @@ "home.loadTutorials.unableToLoadErrorMessage": "无法加载教程", "home.pageTitle": "主页", "home.recentlyAccessed.recentlyViewedTitle": "最近查看", - "home.sampleData.ecommerceSpec.averageSalesPerRegionTitle": "[电子商务] 每地区平均销售额", "home.sampleData.ecommerceSpec.averageSalesPriceTitle": "[电子商务] 平均销售价格", "home.sampleData.ecommerceSpec.averageSoldQuantityTitle": "[电子商务] 平均销售数量", "home.sampleData.ecommerceSpec.controlsTitle": "[电子商务] 控制", @@ -1678,7 +1634,6 @@ "home.sampleData.flightsSpec.globalFlightDashboardDescription": "分析 ES-Air、Logstash Airways、Kibana Airlines 和 JetBeats 的模拟航班数据", "home.sampleData.flightsSpec.globalFlightDashboardTitle": "[航班] 全球航班仪表板", "home.sampleData.flightsSpec.markdownInstructionsTitle": "[航班] Markdown 说明", - "home.sampleData.flightsSpec.originCountryTicketPricesTitle": "[航班] 始发国/地区票价", "home.sampleData.flightsSpec.originCountryTitle": "[航班] 始发国/地区与到达国/地区", "home.sampleData.flightsSpec.totalFlightCancellationsTitle": "[航班] 航班取消总数", "home.sampleData.flightsSpec.totalFlightDelaysTitle": "[航班] 航班延误总数", @@ -1693,7 +1648,6 @@ "home.sampleData.logsSpec.markdownInstructionsTitle": "[日志] Markdown 说明", "home.sampleData.logsSpec.responseCodesOverTimeTitle": "[日志] 时移响应代码 + 注释", "home.sampleData.logsSpec.sourceAndDestinationSankeyChartTitle": "[日志] 始发地和到达地 Sankey 图", - "home.sampleData.logsSpec.uniqueVisitorsByCountryTitle": "[日志] 按国家/地区划分的独立访客", "home.sampleData.logsSpec.uniqueVisitorsTitle": "[日志] 独立访客与平均字节数", "home.sampleData.logsSpec.visitorOSTitle": "[日志] 按 OS 划分的访客", "home.sampleData.logsSpec.webTrafficDescription": "分析 Elastic 网站的模拟 Web 流量日志数据", @@ -4434,8 +4388,6 @@ "visualizations.newVisWizard.searchSelection.savedObjectType.search": "已保存搜索", "visualizations.newVisWizard.selectVisType": "选择可视化类型", "visualizations.newVisWizard.title": "新建可视化", - "visualizations.newVisWizard.visTypeAliasDescription": "打开 Visualize 外部的 Kibana 应用程序。", - "visualizations.newVisWizard.visTypeAliasTitle": "Kibana 应用程序", "visualizations.savedObjectName": "可视化", "visualizations.visualizationTypeInvalidMessage": "无效的可视化类型“{visType}”", "visualize.badge.readOnly.text": "只读", @@ -4534,7 +4486,6 @@ "xpack.actions.serverSideErrors.predefinedActionUpdateDisabled": "不允许更新预配置的操作 {id}。", "xpack.actions.serverSideErrors.unavailableLicenseErrorMessage": "操作类型 {actionTypeId} 已禁用,因为许可证信息当前不可用。", "xpack.actions.serverSideErrors.unavailableLicenseInformationErrorMessage": "操作不可用 - 许可信息当前不可用。", - "xpack.actions.urlAllowedHostsConfigurationError": "目标 {field}“{value}”在 Kibana 配置 xpack.actions.allowedHosts 中未列入白名单", "xpack.alertingBuiltins.indexThreshold.actionGroupThresholdMetTitle": "阈值已达到", "xpack.alertingBuiltins.indexThreshold.actionVariableContextDateLabel": "告警超过阈值的日期。", "xpack.alertingBuiltins.indexThreshold.actionVariableContextGroupLabel": "超过阈值的组。", @@ -4680,12 +4631,17 @@ "xpack.apm.agentMetrics.java.threadCountMax": "最大计数", "xpack.apm.alertTypes.errorRate": "错误率", "xpack.apm.alertTypes.transactionDuration": "事务持续时间", + "xpack.apm.anomaly_detection.error.invalid_license": "要使用异常检测,必须订阅 Elastic 白金级许可证。有了该许可证,您便可借助 Machine Learning 监测服务。", + "xpack.apm.anomaly_detection.error.missing_read_privileges": "必须对 Machine Learning 和 APM 具有“读”权限,才能查看“异常检测”作业", + "xpack.apm.anomaly_detection.error.missing_write_privileges": "必须对 Machine Learning 和 APM 具有“写”权限,才能创建“异常检测”作业", + "xpack.apm.anomaly_detection.error.not_available": "Machine Learning 不可用", + "xpack.apm.anomaly_detection.error.not_available_in_space": "Machine learning 在选定工作区内不可用", "xpack.apm.anomalyDetection.createJobs.failed.text": "为 APM 服务环境 [{environments}] 创建一个或多个异常检测作业时出现问题。错误:“{errorMessage}”", "xpack.apm.anomalyDetection.createJobs.failed.title": "无法创建异常检测作业", "xpack.apm.anomalyDetection.createJobs.succeeded.text": "APM 服务环境 [{environments}] 的异常检测作业已成功创建。Machine Learning 要过一些时间才会开始分析流量以发现异常。", "xpack.apm.anomalyDetection.createJobs.succeeded.title": "异常检测作业已创建", "xpack.apm.anomalyDetectionSetup.linkLabel": "异常检测", - "xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText": "还没有为“{currentEnvironment}”环境启用异常检测。单击以继续设置。", + "xpack.apm.anomalyDetectionSetup.notEnabledForEnvironmentText": "尚未针对环境“{currentEnvironment}”启用异常检测。单击可继续设置。", "xpack.apm.anomalyDetectionSetup.notEnabledText": "异常检测尚未启用。单击以继续设置。", "xpack.apm.apmDescription": "自动从您的应用程序内收集深入全面的性能指标和错误。", "xpack.apm.applyFilter": "应用 {title} 筛选", @@ -4727,7 +4683,7 @@ "xpack.apm.errorGroupDetails.logMessageLabel": "日志消息", "xpack.apm.errorGroupDetails.noErrorsLabel": "未找到任何错误", "xpack.apm.errorGroupDetails.occurrencesChartLabel": "发生次数", - "xpack.apm.errorGroupDetails.occurrencesLongLabel": "{occCount} 次发生", + "xpack.apm.errorGroupDetails.occurrencesLongLabel": "{occCount} 次{occCount, plural, one {出现} other {出现}}", "xpack.apm.errorGroupDetails.occurrencesShortLabel": "{occCount} 次发生", "xpack.apm.errorGroupDetails.relatedTransactionSample": "相关的事务样本", "xpack.apm.errorGroupDetails.unhandledLabel": "未处理", @@ -4754,7 +4710,6 @@ "xpack.apm.fetcher.error.title": "提取资源时出错", "xpack.apm.fetcher.error.url": "URL", "xpack.apm.filter.environment.label": "环境", - "xpack.apm.filter.environment.allLabel": "全部", "xpack.apm.filter.environment.notDefinedLabel": "未定义", "xpack.apm.filter.environment.selectEnvironmentLabel": "选择环境", "xpack.apm.formatters.hoursTimeUnitLabel": "h", @@ -4877,7 +4832,10 @@ "xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric": "分数(最大)", "xpack.apm.serviceMap.anomalyDetectionPopoverTitle": "异常检测", "xpack.apm.serviceMap.anomalyDetectionPopoverTooltip": "服务运行状况指示由 Machine Learning 中的异常检测功能提供", + "xpack.apm.serviceMap.avgCpuUsagePopoverStat": "CPU 使用率(平均值)", + "xpack.apm.serviceMap.avgMemoryUsagePopoverStat": "内存使用率(平均值)", "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "每分钟请求数(平均)", + "xpack.apm.serviceMap.avgTransDurationPopoverStat": "事务持续时间(平均值)", "xpack.apm.serviceMap.betaBadge": "公测版", "xpack.apm.serviceMap.betaTooltipMessage": "此功能当前为公测版。如果遇到任何错误或有任何反馈,请报告问题或访问我们的论坛。", "xpack.apm.serviceMap.center": "中", @@ -4885,10 +4843,13 @@ "xpack.apm.serviceMap.emptyBanner.docsLink": "在文档中了解详情", "xpack.apm.serviceMap.emptyBanner.message": "如果可以检测到连接的服务和外部请求,便将在地图上绘出它们。请确保运行最新版本的 APM 代理。", "xpack.apm.serviceMap.emptyBanner.title": "似乎仅有一个服务。", + "xpack.apm.serviceMap.errorRatePopoverStat": "事务错误率(平均值)", "xpack.apm.serviceMap.focusMapButtonText": "聚焦地图", "xpack.apm.serviceMap.invalidLicenseMessage": "要访问服务地图,必须订阅 Elastic 白金级许可证。使用该许可证,您将能够可视化整个应用程序堆栈以及 APM 数据。", "xpack.apm.serviceMap.popoverMetrics.noDataText": "选定的环境没有数据。请尝试切换到其他环境。", "xpack.apm.serviceMap.serviceDetailsButtonText": "服务详情", + "xpack.apm.serviceMap.subtypePopoverStat": "子类型", + "xpack.apm.serviceMap.typePopoverStat": "类型", "xpack.apm.serviceMap.viewFullMap": "查看完整的服务地图", "xpack.apm.serviceMap.zoomIn": "放大", "xpack.apm.serviceMap.zoomOut": "缩小", @@ -5408,7 +5369,7 @@ "xpack.canvas.elementSettings.dataTabLabel": "数据", "xpack.canvas.elementSettings.displayTabLabel": "显示", "xpack.canvas.embedObject.noMatchingObjectsMessage": "未找到任何匹配对象。", - "xpack.canvas.embedObject.titleText": "从 Visualize 库添加", + "xpack.canvas.embedObject.titleText": "从 Kibana 添加", "xpack.canvas.error.actionsElements.invaludArgIndexErrorMessage": "无效的参数索引:{index}", "xpack.canvas.error.downloadWorkpad.downloadFailureErrorMessage": "无法下载 Workpad", "xpack.canvas.error.downloadWorkpad.downloadRenderedWorkpadFailureErrorMessage": "无法下载已呈现 Workpad", @@ -6296,7 +6257,7 @@ "xpack.canvas.workpadHeaderElementMenu.chartMenuItemLabel": "图表", "xpack.canvas.workpadHeaderElementMenu.elementMenuButtonLabel": "添加元素", "xpack.canvas.workpadHeaderElementMenu.elementMenuLabel": "添加元素", - "xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel": "从 Visualize 库添加", + "xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel": "从 Kibana 添加", "xpack.canvas.workpadHeaderElementMenu.filterMenuItemLabel": "筛选", "xpack.canvas.workpadHeaderElementMenu.imageMenuItemLabel": "图像", "xpack.canvas.workpadHeaderElementMenu.manageAssetsMenuItemLabel": "管理资产", @@ -6748,6 +6709,9 @@ "xpack.enterpriseSearch.errorConnectingState.description4": "阅读设置指南或查看服务器日志中的 {pluginLog} 日志消息。", "xpack.enterpriseSearch.errorConnectingState.setupGuideCta": "阅读设置指南", "xpack.enterpriseSearch.errorConnectingState.title": "无法连接", + "xpack.enterpriseSearch.errorConnectingState.troubleshootAuth": "检查您的用户身份验证:", + "xpack.enterpriseSearch.errorConnectingState.troubleshootAuthNative": "必须使用 Elasticsearch 本机身份验证或 SSO/SAML 执行身份验证。", + "xpack.enterpriseSearch.errorConnectingState.troubleshootAuthSAML": "如果使用的是 SSO/SAML,则还必须在“企业搜索”中设置 SAML 领域。", "xpack.enterpriseSearch.setupGuide.step1.instruction1": "在 {configFile} 文件中,将 {configSetting} 设置为 {productName} 实例的 URL。例如:", "xpack.enterpriseSearch.setupGuide.step1.title": "将 {productName} 主机 URL 添加到 Kibana 配置", "xpack.enterpriseSearch.setupGuide.step2.instruction1": "重新启动 Kibana 以应用上一步骤中的配置更改。", @@ -8103,7 +8067,6 @@ "xpack.indexLifecycleMgmt.editPolicy.learnAboutShardAllocationLink": "了解分片分配", "xpack.indexLifecycleMgmt.editPolicy.learnAboutTimingText": "了解计时", "xpack.indexLifecycleMgmt.editPolicy.lifecyclePolicyDescriptionText": "使用索引策略自动化索引生命周期的四个阶段,从频繁地写入到索引到删除索引。", - "xpack.indexLifecycleMgmt.editPolicy.loadPolicyErrorMessage": "加载策略时出错", "xpack.indexLifecycleMgmt.editPolicy.maximumAgeMissingError": "最大存在时间必填。", "xpack.indexLifecycleMgmt.editPolicy.maximumDocumentsMissingError": "最大文档数必填。", "xpack.indexLifecycleMgmt.editPolicy.maximumIndexSizeMissingError": "最大索引大小必填。", @@ -8242,7 +8205,6 @@ "xpack.indexLifecycleMgmt.policyTable.addLifecyclePolicyToTemplateConfirmModal.title": "将策略 “{name}” 添加到索引模板", "xpack.indexLifecycleMgmt.policyTable.addPolicyToTemplateButtonText": "将策略添加到索引模板", "xpack.indexLifecycleMgmt.policyTable.captionText": "下面是包含 {count, plural, one {# 行} other {# 行}}(共 {total} 行)的索引生命周期策略表。", - "xpack.indexLifecycleMgmt.policyTable.deletedPoliciesText": "已删除 {numSelected} 个{numSelected, plural, one {策略} other {策略}}", "xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonDisabledTooltip": "您无法删除索引正在使用的策略", "xpack.indexLifecycleMgmt.policyTable.deletePolicyButtonText": "删除策略", "xpack.indexLifecycleMgmt.policyTable.emptyPrompt.createButtonLabel": "创建策略", @@ -8467,6 +8429,7 @@ "xpack.infra.logs.analysis.anomalySectionLogRateChartNoData": "没有要显示的日志速率数据。", "xpack.infra.logs.analysis.anomalySectionNoDataBody": "您可能想调整时间范围。", "xpack.infra.logs.analysis.anomalySectionNoDataTitle": "没有可显示的数据。", + "xpack.infra.logs.analysis.createJobButtonLabel": "创建 ML 作业", "xpack.infra.logs.analysis.datasetFilterPlaceholder": "按数据集筛选", "xpack.infra.logs.analysis.enableAnomalyDetectionButtonLabel": "启用异常检测", "xpack.infra.logs.analysis.jobConfigurationOutdatedCalloutMessage": "创建 {moduleName} ML 作业时所使用的源配置不同。重新创建作业以应用当前配置。这将移除以前检测到的异常。", @@ -8482,6 +8445,9 @@ "xpack.infra.logs.analysis.logEntryRateModuleDescription": "使用 Machine Learning 自动检测异常日志条目速率。", "xpack.infra.logs.analysis.logEntryRateModuleName": "日志速率", "xpack.infra.logs.analysis.manageMlJobsButtonLabel": "管理 ML 作业", + "xpack.infra.logs.analysis.missingMlPrivilegesTitle": "需要其他 Machine Learning 权限", + "xpack.infra.logs.analysis.missingMlResultsPrivilegesDescription": "此功能使用 Machine Learning 作业,这需要对 Machine Learning 应用至少有读权限,才能访问这些作业的状态和结果。", + "xpack.infra.logs.analysis.missingMlSetupPrivilegesDescription": "此功能使用 Machine Learning 作业,这需要对 Machine Learning 应用具有所有权限,才能进行相应的设置。", "xpack.infra.logs.analysis.mlAppButton": "打开 Machine Learning", "xpack.infra.logs.analysis.mlUnavailableBody": "查看 {machineLearningAppLink} 以获取更多信息。", "xpack.infra.logs.analysis.mlUnavailableTitle": "此功能需要 Machine Learning", @@ -8532,7 +8498,7 @@ "xpack.infra.logs.logAnalysis.splash.loadingMessage": "正在检查许可证......", "xpack.infra.logs.logAnalysis.splash.splashImageAlt": "占位符图像", "xpack.infra.logs.logAnalysis.splash.startTrialCta": "开始试用", - "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "我们为期 14 天的免费试用包含 Machine Learning 功能,允许您在日志中检测异常。", + "xpack.infra.logs.logAnalysis.splash.startTrialDescription": "我们的免费试用版包含 Machine Learning 功能,可用于检测日志中的异常。", "xpack.infra.logs.logAnalysis.splash.startTrialTitle": "要使用异常检测,请开始免费的试用", "xpack.infra.logs.logAnalysis.splash.updateSubscriptionCta": "升级订阅", "xpack.infra.logs.logAnalysis.splash.updateSubscriptionDescription": "必须具有白金级订阅,才能使用 Machine Learning 功能。", @@ -8731,11 +8697,12 @@ "xpack.infra.metrics.alertFlyout.alertName": "指标阈值", "xpack.infra.metrics.alertFlyout.alertOnNoData": "没数据时提醒我", "xpack.infra.metrics.alertFlyout.alertPreviewError": "尝试预览此告警条件时发生错误", + "xpack.infra.metrics.alertFlyout.alertPreviewErrorDesc": "请稍后重试或查看详情了解更多信息。", "xpack.infra.metrics.alertFlyout.alertPreviewErrorResult": "尝试评估部分数据时发生错误。", - "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups} 个{groupName}", + "xpack.infra.metrics.alertFlyout.alertPreviewGroups": "{numberOfGroups, plural, one {# 个 {groupName}} other {# 个 {groupName}}}", "xpack.infra.metrics.alertFlyout.alertPreviewGroupsAcross": "在", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "存在 {boldedResultsNumber} 个无数据结果。", - "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber": "{noData}", + "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResult": "存在 {boldedResultsNumber}无数据结果。", + "xpack.infra.metrics.alertFlyout.alertPreviewNoDataResultNumber": "{noData, plural, one {# 个} other {# 个}}", "xpack.infra.metrics.alertFlyout.alertPreviewResult": "此告警将发生 {firedTimes}", "xpack.infra.metrics.alertFlyout.alertPreviewResultLookback": "在过去 {lookback}。", "xpack.infra.metrics.alertFlyout.conditions": "条件", @@ -8747,6 +8714,7 @@ "xpack.infra.metrics.alertFlyout.error.thresholdRequired": "“阈值”必填。", "xpack.infra.metrics.alertFlyout.error.thresholdTypeRequired": "阈值必须包含有效数字。", "xpack.infra.metrics.alertFlyout.error.timeRequred": "“时间大小”必填。", + "xpack.infra.metrics.alertFlyout.errorDetails": "详情", "xpack.infra.metrics.alertFlyout.expandRowLabel": "展开行。", "xpack.infra.metrics.alertFlyout.expression.for.descriptionLabel": "对于", "xpack.infra.metrics.alertFlyout.expression.for.popoverTitle": "库存类型", @@ -8773,8 +8741,12 @@ "xpack.infra.metrics.alertFlyout.tooManyBucketsErrorDescription": "尝试选择较短的预览长度或在“{forTheLast}”字段中增大时间量。", "xpack.infra.metrics.alertFlyout.tooManyBucketsErrorTitle": "数据过多(>{maxBuckets} 个结果)", "xpack.infra.metrics.alertFlyout.weekLabel": "周", + "xpack.infra.metrics.alerting.alertStateActionVariableDescription": "告警的当前状态", + "xpack.infra.metrics.alerting.groupActionVariableDescription": "报告数据的组名称", "xpack.infra.metrics.alerting.inventory.threshold.defaultActionMessage": "\\{\\{alertName\\}\\} - \\{\\{context.group\\}\\} 处于 \\{\\{context.alertState\\}\\} 状态\n\n原因:\n\\{\\{context.reason\\}\\}\n", "xpack.infra.metrics.alerting.inventory.threshold.fired": "已触发", + "xpack.infra.metrics.alerting.metricActionVariableDescription": "指定条件中的指标名称。用法:(ctx.metric.condition0, ctx.metric.condition1, 诸如此类)。", + "xpack.infra.metrics.alerting.reasonActionVariableDescription": "描述告警处于此状态的原因,包括哪些指标已超过哪些阈值", "xpack.infra.metrics.alerting.threshold.aboveRecovery": "高于", "xpack.infra.metrics.alerting.threshold.alertState": "告警", "xpack.infra.metrics.alerting.threshold.belowRecovery": "低于", @@ -8795,6 +8767,9 @@ "xpack.infra.metrics.alerting.threshold.outsideRangeComparator": "不介于", "xpack.infra.metrics.alerting.threshold.recoveredAlertReason": "{metric} 现在{comparator}阈值 {threshold}(当前值为 {currentValue})", "xpack.infra.metrics.alerting.threshold.thresholdRange": "{a} 和 {b}", + "xpack.infra.metrics.alerting.thresholdActionVariableDescription": "指定条件中的指标阈值。用法:(ctx.threshold.condition0, ctx.threshold.condition1, 诸如此类)。", + "xpack.infra.metrics.alerting.timestampDescription": "检测到告警时的时间戳。", + "xpack.infra.metrics.alerting.valueActionVariableDescription": "指定条件中的指标值。用法:(ctx.value.condition0, ctx.value.condition1, 诸如此类)。", "xpack.infra.metrics.alerts.dataTimeRangeLabel": "过去 {lookback} {timeLabel}", "xpack.infra.metrics.alerts.dataTimeRangeLabelWithGrouping": "{id} 过去 {lookback} {timeLabel}的数据", "xpack.infra.metrics.alerts.loadingMessage": "正在加载", @@ -8813,7 +8788,6 @@ "xpack.infra.metrics.missingTSVBModelError": "{nodeType} 的 {metricId} TSVB 模型不存在", "xpack.infra.metrics.pluginTitle": "指标", "xpack.infra.metrics.refetchButtonLabel": "检查新数据", - "xpack.infra.metricsExplorer.everything": "所有内容", "xpack.infra.metricsExplorer.actionsLabel.aria": "适用于 {grouping} 的操作", "xpack.infra.metricsExplorer.actionsLabel.button": "操作", "xpack.infra.metricsExplorer.aggregationLabel": "的", @@ -8905,7 +8879,7 @@ "xpack.infra.savedView.manageViews": "管理视图", "xpack.infra.savedView.saveNewView": "保存新视图", "xpack.infra.savedView.searchPlaceholder": "搜索已保存视图", - "xpack.infra.savedView.unknownView": "未知", + "xpack.infra.savedView.unknownView": "未选择视图", "xpack.infra.savedView.updateView": "更新视图", "xpack.infra.snapshot.missingSnapshotMetricError": "{nodeType} 的 {metric} 聚合不可用。", "xpack.infra.sourceConfiguration.addLogColumnButtonLabel": "添加列", @@ -9056,54 +9030,11 @@ "xpack.infra.waffle.unableToSelectMetricErrorTitle": "无法选择指标选项或指标值。", "xpack.infra.waffleTime.autoRefreshButtonLabel": "自动刷新", "xpack.infra.waffleTime.stopRefreshingButtonLabel": "停止刷新", - "xpack.ingestManager.agentPolicy.confirmModalCalloutDescription": "Fleet 检测到所选代理配置 {policyName} 已由您的部分代理使用。由于此操作,Fleet 会将更新部署到使用此配置的所有代理。", - "xpack.ingestManager.agentPolicy.confirmModalCancelButtonLabel": "取消", - "xpack.ingestManager.agentPolicy.confirmModalConfirmButtonLabel": "保存并部署更改", - "xpack.ingestManager.agentPolicy.confirmModalDescription": "此操作无法撤消。是否确定要继续?", - "xpack.ingestManager.agentPolicy.confirmModalTitle": "保存并部署更改", - "xpack.ingestManager.agentPolicy.linkedAgentCountText": "{count, plural, one {# 个代理} other {# 个代理}}", - "xpack.ingestManager.agentPolicyActionMenu.buttonText": "操作", - "xpack.ingestManager.agentPolicyActionMenu.copyPolicyActionText": "复制配置", - "xpack.ingestManager.agentPolicyActionMenu.enrollAgentActionText": "添加代理", - "xpack.ingestManager.agentPolicyActionMenu.viewPolicyText": "查看配置", - "xpack.ingestManager.agentPolicyForm.advancedOptionsToggleLabel": "高级选项", - "xpack.ingestManager.agentPolicyForm.descriptionFieldLabel": "描述", - "xpack.ingestManager.agentPolicyForm.descriptionFieldPlaceholder": "此配置将如何使用?", - "xpack.ingestManager.agentPolicyForm.monitoringDescription": "收集有关代理的数据,以排查和跟踪性能。", - "xpack.ingestManager.agentPolicyForm.monitoringLabel": "代理监测", - "xpack.ingestManager.agentPolicyForm.monitoringLogsFieldLabel": "收集代理日志", - "xpack.ingestManager.agentPolicyForm.monitoringMetricsFieldLabel": "收集代理指标", - "xpack.ingestManager.agentPolicyForm.nameFieldLabel": "名称", - "xpack.ingestManager.agentPolicyForm.nameFieldPlaceholder": "选择名称", - "xpack.ingestManager.agentPolicyForm.nameRequiredErrorMessage": "“代理配置名称”必填", - "xpack.ingestManager.agentPolicyForm.namespaceFieldDescription": "将默认命名空间应用于使用此配置的集成。集成可以指定自己的命名空间。", - "xpack.ingestManager.agentPolicyForm.namespaceFieldLabel": "默认命名空间", - "xpack.ingestManager.agentPolicyForm.namespaceRequiredErrorMessage": "命名空间必填", - "xpack.ingestManager.agentPolicyForm.systemMonitoringFieldLabel": "可选", - "xpack.ingestManager.agentPolicyForm.systemMonitoringText": "收集系统指标", - "xpack.ingestManager.agentPolicyForm.systemMonitoringTooltipText": "启用此选项可使用收集系统指标和信息的集成启动您的配置。", - "xpack.ingestManager.agentPolicyList.actionsColumnTitle": "操作", - "xpack.ingestManager.agentPolicyList.addButton": "创建代理配置", - "xpack.ingestManager.agentPolicyList.agentsColumnTitle": "代理", - "xpack.ingestManager.agentPolicyList.clearFiltersLinkText": "清除筛选", - "xpack.ingestManager.agentPolicyList.descriptionColumnTitle": "描述", - "xpack.ingestManager.agentPolicyList.loadingAgentPoliciesMessage": "正在加载代理配置……", - "xpack.ingestManager.agentPolicyList.nameColumnTitle": "名称", - "xpack.ingestManager.agentPolicyList.noAgentPoliciesPrompt": "无代理配置", - "xpack.ingestManager.agentPolicyList.noFilteredAgentPoliciesPrompt": "找不到代理配置。{clearFiltersLink}", - "xpack.ingestManager.agentPolicyList.packagePoliciesCountColumnTitle": "集成", - "xpack.ingestManager.agentPolicyList.pageSubtitle": "使用代理配置管理代理和它们收集的数据。", - "xpack.ingestManager.agentPolicyList.pageTitle": "代理配置", - "xpack.ingestManager.agentPolicyList.reloadAgentPoliciesButtonText": "重新加载", - "xpack.ingestManager.agentPolicyList.revisionNumber": "修订 {revNumber}", - "xpack.ingestManager.agentPolicyList.updatedOnColumnTitle": "最后更新时间", "xpack.ingestManager.agentDetails.actionsButton": "操作", - "xpack.ingestManager.agentDetails.agentPolicyLabel": "代理配置", "xpack.ingestManager.agentDetails.agentDetailsTitle": "代理“{id}”", "xpack.ingestManager.agentDetails.agentNotFoundErrorDescription": "找不到代理 ID {agentId}", "xpack.ingestManager.agentDetails.agentNotFoundErrorTitle": "未找到代理", - "xpack.ingestManager.agentDetails.policyLabel": "配置", - "xpack.ingestManager.agentDetails.hostIdLabel": "主机 ID", + "xpack.ingestManager.agentDetails.hostIdLabel": "代理 ID", "xpack.ingestManager.agentDetails.hostNameLabel": "主机名", "xpack.ingestManager.agentDetails.localMetadataSectionSubtitle": "本地元数据", "xpack.ingestManager.agentDetails.metadataSectionTitle": "元数据", @@ -9117,21 +9048,16 @@ "xpack.ingestManager.agentDetails.viewAgentListTitle": "查看所有代理", "xpack.ingestManager.agentEnrollment.cancelButtonLabel": "取消", "xpack.ingestManager.agentEnrollment.continueButtonLabel": "继续", - "xpack.ingestManager.agentEnrollment.copyPolicyButton": "复制到剪贴板", "xpack.ingestManager.agentEnrollment.copyRunInstructionsButton": "复制到剪贴板", - "xpack.ingestManager.agentEnrollment.downloadPolicyButton": "下载配置", "xpack.ingestManager.agentEnrollment.downloadDescription": "在主机计算机上下载 Elastic 代理。可以从 Elastic 代理下载页面访问代理二进制文件及其验证签名。", "xpack.ingestManager.agentEnrollment.downloadLink": "前往 elastic.co/downloads", "xpack.ingestManager.agentEnrollment.enrollFleetTabLabel": "注册到 Fleet", "xpack.ingestManager.agentEnrollment.enrollStandaloneTabLabel": "独立模式", "xpack.ingestManager.agentEnrollment.fleetNotInitializedText": "注册代理前需要设置 Fleet。{link}", "xpack.ingestManager.agentEnrollment.flyoutTitle": "添加代理", - "xpack.ingestManager.agentEnrollment.goToFleetButton": "前往 Fleet。", "xpack.ingestManager.agentEnrollment.managedDescription": "无论是需要一个代理还是需要数以千计的代理,Fleet 允许您轻松地集中管理并部署代理的更新。按照下面的说明下载 Elastic 代理并将代理注册到 Fleet。", "xpack.ingestManager.agentEnrollment.standaloneDescription": "如果希望对以独立模式运行的代理进行配置更改,则需要手动更新。按照下面的说明下载并设置独立模式的 Elastic 代理。", - "xpack.ingestManager.agentEnrollment.stepCheckForDataDescription": "启动代理后,代理应开始发送数据。您可以在采集管理器的数据集页面查看此数据。", "xpack.ingestManager.agentEnrollment.stepCheckForDataTitle": "检查数据", - "xpack.ingestManager.agentEnrollment.stepChooseAgentPolicyTitle": "选择代理配置", "xpack.ingestManager.agentEnrollment.stepConfigureAgentDescription": "在安装 Elastic 代理的系统上复制此配置并将其放入名为 {fileName} 的文件中。切勿忘记修改配置文件中 {outputSection} 部分的{ESUsernameVariable} 和 {ESPasswordVariable},以便其使用您的实际 Elasticsearch 凭据。", "xpack.ingestManager.agentEnrollment.stepConfigureAgentTitle": "配置代理", "xpack.ingestManager.agentEnrollment.stepDownloadAgentTitle": "下载 Elastic 代理", @@ -9149,8 +9075,8 @@ "xpack.ingestManager.agentEventsList.timestampColumnTitle": "时间戳", "xpack.ingestManager.agentEventsList.typeColumnTitle": "类型", "xpack.ingestManager.agentEventSubtype.acknowledgedLabel": "已确认", - "xpack.ingestManager.agentEventSubtype.policyLabel": "配置", "xpack.ingestManager.agentEventSubtype.dataDumpLabel": "数据转储", + "xpack.ingestManager.agentEventSubtype.degradedLabel": "已降级", "xpack.ingestManager.agentEventSubtype.failedLabel": "失败", "xpack.ingestManager.agentEventSubtype.inProgressLabel": "进行中", "xpack.ingestManager.agentEventSubtype.runningLabel": "正在运行", @@ -9175,9 +9101,8 @@ "xpack.ingestManager.agentList.actionsColumnTitle": "操作", "xpack.ingestManager.agentList.addButton": "添加代理", "xpack.ingestManager.agentList.clearFiltersLinkText": "清除筛选", - "xpack.ingestManager.agentList.policyColumnTitle": "代理配置", - "xpack.ingestManager.agentList.policyFilterText": "代理配置", "xpack.ingestManager.agentList.enrollButton": "添加代理", + "xpack.ingestManager.agentList.forceUnenrollOneButton": "强制取消注册", "xpack.ingestManager.agentList.hostColumnTitle": "主机", "xpack.ingestManager.agentList.lastCheckinTitle": "上次活动", "xpack.ingestManager.agentList.loadingAgentsMessage": "正在加载代理……", @@ -9199,13 +9124,6 @@ "xpack.ingestManager.agentListStatus.offlineLabel": "脱机", "xpack.ingestManager.agentListStatus.onlineLabel": "联机", "xpack.ingestManager.agentListStatus.totalLabel": "代理", - "xpack.ingestManager.agentReassignPolicy.cancelButtonLabel": "取消", - "xpack.ingestManager.agentReassignPolicy.policyDescription": "选定代理配置将收集 {count, plural, one {{countValue} 个集成} other {{countValue} 个集成}}的数据:", - "xpack.ingestManager.agentReassignPolicy.continueButtonLabel": "分配配置", - "xpack.ingestManager.agentReassignPolicy.flyoutDescription": "选择要为选定代理分配的新代理配置。", - "xpack.ingestManager.agentReassignPolicy.flyoutTitle": "分配新代理配置", - "xpack.ingestManager.agentReassignPolicy.selectPolicyLabel": "代理配置", - "xpack.ingestManager.agentReassignPolicy.successSingleNotificationTitle": "代理配置已重新分配", "xpack.ingestManager.alphaMessageDescription": "不推荐在生产环境中使用采集管理器。", "xpack.ingestManager.alphaMessageLinkText": "查看更多详情。", "xpack.ingestManager.alphaMessageTitle": "公测版", @@ -9215,7 +9133,6 @@ "xpack.ingestManager.alphaMessaging.forumLink": "讨论论坛", "xpack.ingestManager.alphaMessaging.introText": "采集管理器仍处于开发状态,不适用于生产环境。此公测版用于用户测试采集管理器和新 Elastic 代理并提供相关反馈。此插件不受支持 SLA 的约束。", "xpack.ingestManager.alphaMessging.closeFlyoutLabel": "关闭", - "xpack.ingestManager.appNavigation.policiesLinkText": "配置", "xpack.ingestManager.appNavigation.dataStreamsLinkText": "数据集", "xpack.ingestManager.appNavigation.epmLinkText": "集成", "xpack.ingestManager.appNavigation.fleetLinkText": "Fleet", @@ -9225,102 +9142,15 @@ "xpack.ingestManager.appTitle": "Ingest Manager", "xpack.ingestManager.betaBadge.labelText": "公测版", "xpack.ingestManager.betaBadge.tooltipText": "不推荐在生产环境中使用此插件。请在我们讨论论坛中报告错误。", - "xpack.ingestManager.breadcrumbs.addPackagePolicyPageTitle": "添加集成", "xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle": "全部", "xpack.ingestManager.breadcrumbs.appTitle": "采集管理器", - "xpack.ingestManager.breadcrumbs.policiesPageTitle": "配置", "xpack.ingestManager.breadcrumbs.datastreamsPageTitle": "数据集", - "xpack.ingestManager.breadcrumbs.editPackagePolicyPageTitle": "编辑集成", "xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle": "代理", "xpack.ingestManager.breadcrumbs.fleetEnrollmentTokensPageTitle": "注册令牌", "xpack.ingestManager.breadcrumbs.fleetPageTitle": "Fleet", "xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle": "已安装", "xpack.ingestManager.breadcrumbs.integrationsPageTitle": "集成", "xpack.ingestManager.breadcrumbs.overviewPageTitle": "概览", - "xpack.ingestManager.policyDetails.addPackagePolicyButtonText": "添加集成", - "xpack.ingestManager.policyDetails.policyDetailsTitle": "配置“{id}”", - "xpack.ingestManager.policyDetails.policyNotFoundErrorTitle": "未找到配置“{id}”", - "xpack.ingestManager.policyDetails.packagePoliciesTable.actionsColumnTitle": "操作", - "xpack.ingestManager.policyDetails.packagePoliciesTable.deleteActionTitle": "删除集成", - "xpack.ingestManager.policyDetails.packagePoliciesTable.descriptionColumnTitle": "描述", - "xpack.ingestManager.policyDetails.packagePoliciesTable.editActionTitle": "编辑集成", - "xpack.ingestManager.policyDetails.packagePoliciesTable.nameColumnTitle": "名称", - "xpack.ingestManager.policyDetails.packagePoliciesTable.namespaceColumnTitle": "命名空间", - "xpack.ingestManager.policyDetails.packagePoliciesTable.packageNameColumnTitle": "集成", - "xpack.ingestManager.policyDetails.subTabs.packagePoliciesTabText": "集成", - "xpack.ingestManager.policyDetails.subTabs.settingsTabText": "设置", - "xpack.ingestManager.policyDetails.summary.lastUpdated": "最后更新时间", - "xpack.ingestManager.policyDetails.summary.integrations": "集成", - "xpack.ingestManager.policyDetails.summary.revision": "修订", - "xpack.ingestManager.policyDetails.summary.usedBy": "使用者", - "xpack.ingestManager.policyDetails.unexceptedErrorTitle": "加载配置时发生错误", - "xpack.ingestManager.policyDetails.viewAgentListTitle": "查看所有代理配置", - "xpack.ingestManager.policyDetails.yamlDownloadButtonLabel": "下载配置", - "xpack.ingestManager.policyDetails.yamlFlyoutCloseButtonLabel": "关闭", - "xpack.ingestManager.policyDetails.yamlflyoutTitleWithName": "“{name}”代理配置", - "xpack.ingestManager.policyDetails.yamlflyoutTitleWithoutName": "代理配置", - "xpack.ingestManager.policyDetailsPackagePolicies.createFirstButtonText": "添加集成", - "xpack.ingestManager.policyDetailsPackagePolicies.createFirstMessage": "此配置尚未有任何集成。", - "xpack.ingestManager.policyDetailsPackagePolicies.createFirstTitle": "添加您的首个集成", - "xpack.ingestManager.policyForm.deletePolicyActionText": "删除配置", - "xpack.ingestManager.policyForm.deletePolicyGroupDescription": "将不会删除现有数据。", - "xpack.ingestManager.policyForm.deletePolicyGroupTitle": "删除配置", - "xpack.ingestManager.policyForm.generalSettingsGroupDescription": "为您的代理配置选择名称和描述。", - "xpack.ingestManager.policyForm.generalSettingsGroupTitle": "常规设置", - "xpack.ingestManager.policyForm.unableToDeleteDefaultPolicyText": "无法删除默认配置", - "xpack.ingestManager.copyAgentPolicy.confirmModal.cancelButtonLabel": "取消", - "xpack.ingestManager.copyAgentPolicy.confirmModal.confirmButtonLabel": "复制配置", - "xpack.ingestManager.copyAgentPolicy.confirmModal.copyPolicyPrompt": "为您的新代理配置选择名称和描述。", - "xpack.ingestManager.copyAgentPolicy.confirmModal.copyPolicyTitle": "复制“{name}”代理配置", - "xpack.ingestManager.copyAgentPolicy.confirmModal.defaultNewPolicyName": "{name}(副本)", - "xpack.ingestManager.copyAgentPolicy.confirmModal.newDescriptionLabel": "描述", - "xpack.ingestManager.copyAgentPolicy.confirmModal.newNameLabel": "新配置名称", - "xpack.ingestManager.copyAgentPolicy.failureNotificationTitle": "复制代理配置“{id}”时出错", - "xpack.ingestManager.copyAgentPolicy.fatalErrorNotificationTitle": "复制代理配置时出错", - "xpack.ingestManager.copyAgentPolicy.successNotificationTitle": "代理配置已复制", - "xpack.ingestManager.createAgentPolicy.cancelButtonLabel": "取消", - "xpack.ingestManager.createAgentPolicy.errorNotificationTitle": "无法创建代理配置", - "xpack.ingestManager.createAgentPolicy.flyoutTitle": "创建代理配置", - "xpack.ingestManager.createAgentPolicy.flyoutTitleDescription": "代理配置用于管理整个代理组的设置。您可以将集成添加到代理配置以指定代理收集的数据。编辑代理配置时,可以使用 Fleet 将更新部署到指定的代理组。", - "xpack.ingestManager.createAgentPolicy.submitButtonLabel": "创建代理配置", - "xpack.ingestManager.createAgentPolicy.successNotificationTitle": "代理配置“{name}”已创建", - "xpack.ingestManager.createPackagePolicy.addedNotificationMessage": "Fleet 会将更新部署到所有使用配置“{agentPolicyName}”的代理", - "xpack.ingestManager.createPackagePolicy.addedNotificationTitle": "已成功添加“{packagePolicyName}”", - "xpack.ingestManager.createPackagePolicy.agentPolicyNameLabel": "代理配置", - "xpack.ingestManager.createPackagePolicy.cancelButton": "取消", - "xpack.ingestManager.createPackagePolicy.cancelLinkText": "取消", - "xpack.ingestManager.createPackagePolicy.errorOnSaveText": "您的集成配置有错误。请在保存前进行修复。", - "xpack.ingestManager.createPackagePolicy.pageDescriptionfromPolicy": "配置选定代理配置的集成。", - "xpack.ingestManager.createPackagePolicy.pageDescriptionfromPackage": "按照下面的说明将此集成添加到代理配置。", - "xpack.ingestManager.createPackagePolicy.pageTitle": "添加集成", - "xpack.ingestManager.createPackagePolicy.pageTitleWithPackageName": "添加 {packageName} 集成", - "xpack.ingestManager.createPackagePolicy.saveButton": "保存集成", - "xpack.ingestManager.createPackagePolicy.stepConfigure.advancedOptionsToggleLinkText": "高级选项", - "xpack.ingestManager.createPackagePolicy.stepConfigure.errorCountText": "{count, plural, one {# 个错误} other {# 个错误}}", - "xpack.ingestManager.createPackagePolicy.stepConfigure.hideStreamsAriaLabel": "隐藏 {type} 输入", - "xpack.ingestManager.createPackagePolicy.stepConfigure.inputSettingsDescription": "以下设置适用于下面的所有输入。", - "xpack.ingestManager.createPackagePolicy.stepConfigure.inputSettingsTitle": "设置", - "xpack.ingestManager.createPackagePolicy.stepConfigure.inputVarFieldOptionalLabel": "可选", - "xpack.ingestManager.createPackagePolicy.stepConfigure.integrationSettingsSectionDescription": "选择有助于确定如何使用此集成的名称和描述。", - "xpack.ingestManager.createPackagePolicy.stepConfigure.integrationSettingsSectionTitle": "集成设置", - "xpack.ingestManager.createPackagePolicy.stepConfigure.noPolicyOptionsMessage": "没有可配置的内容", - "xpack.ingestManager.createPackagePolicy.stepConfigure.packagePolicyDescriptionInputLabel": "描述", - "xpack.ingestManager.createPackagePolicy.stepConfigure.packagePolicyNameInputLabel": "集成名称", - "xpack.ingestManager.createPackagePolicy.stepConfigure.packagePolicyNamespaceInputLabel": "命名空间", - "xpack.ingestManager.createPackagePolicy.stepConfigure.showStreamsAriaLabel": "显示 {type} 输入", - "xpack.ingestManager.createPackagePolicy.stepConfigure.toggleAdvancedOptionsButtonText": "高级选项", - "xpack.ingestManager.createPackagePolicy.stepConfigurePackagePolicyTitle": "配置集成", - "xpack.ingestManager.createPackagePolicy.stepSelectAgentPolicyTitle": "选择代理配置", - "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.addButton": "新建代理配置", - "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsCountText": "{count, plural, one {# 个代理} other {# 个代理}}", - "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.errorLoadingAgentPoliciesTitle": "加载代理配置时出错", - "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.errorLoadingPackageTitle": "加载软件包信息时出错", - "xpack.ingestManager.createPackagePolicy.StepSelectPolicy.errorLoadingSelectedAgentPolicyTitle": "加载选定代理配置时出错", - "xpack.ingestManager.createPackagePolicy.stepSelectPackage.errorLoadingPolicyTitle": "加载代理配置信息时出错", - "xpack.ingestManager.createPackagePolicy.stepSelectPackage.errorLoadingPackagesTitle": "加载集成时出错", - "xpack.ingestManager.createPackagePolicy.stepSelectPackage.errorLoadingSelectedPackageTitle": "加载选定集成时出错", - "xpack.ingestManager.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder": "搜索集成", - "xpack.ingestManager.createPackagePolicy.stepSelectPackageTitle": "选择集成", "xpack.ingestManager.dataStreamList.actionsColumnTitle": "操作", "xpack.ingestManager.dataStreamList.datasetColumnTitle": "数据集", "xpack.ingestManager.dataStreamList.integrationColumnTitle": "集成", @@ -9339,73 +9169,34 @@ "xpack.ingestManager.dataStreamList.viewDashboardsActionText": "查看仪表板", "xpack.ingestManager.dataStreamList.viewDashboardsPanelTitle": "查看仪表板", "xpack.ingestManager.defaultSearchPlaceholderText": "搜索", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# 个代理} other {# 个代理}}已分配给此代理配置。在删除此配置之前请取消分配这些代理。", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.affectedAgentsTitle": "配置已被用", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.cancelButtonLabel": "取消", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.confirmButtonLabel": "删除配置", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.deletePolicyTitle": "删除此代理配置?", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.irreversibleMessage": "此操作无法撤消。", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.loadingAgentsCountMessage": "正在检查受影响代理数量……", - "xpack.ingestManager.deleteAgentPolicy.confirmModal.loadingButtonLabel": "正在加载……", - "xpack.ingestManager.deleteAgentPolicy.failureSingleNotificationTitle": "删除代理配置“{id}”时出错", - "xpack.ingestManager.deleteAgentPolicy.fatalErrorNotificationTitle": "删除代理配置时出错", - "xpack.ingestManager.deleteAgentPolicy.successSingleNotificationTitle": "已删除代理配置“{id}”", - "xpack.ingestManager.deletePackagePolicy.confirmModal.affectedAgentsMessage": "Fleet 已检测到 {agentPolicyName} 已由您的部分代理使用。", - "xpack.ingestManager.deletePackagePolicy.confirmModal.affectedAgentsTitle": "此操作将影响 {agentsCount} 个{agentsCount, plural, one {代理} other {代理}}。", - "xpack.ingestManager.deletePackagePolicy.confirmModal.cancelButtonLabel": "取消", - "xpack.ingestManager.deletePackagePolicy.confirmModal.confirmButtonLabel": "删除{agentPoliciesCount, plural, one {集成} other {集成}}", - "xpack.ingestManager.deletePackagePolicy.confirmModal.deleteMultipleTitle": "删除{count, plural, one {集成} other {# 个集成}}?", - "xpack.ingestManager.deletePackagePolicy.confirmModal.generalMessage": "此操作无法撤消。是否确定要继续?", - "xpack.ingestManager.deletePackagePolicy.confirmModal.loadingAgentsCountMessage": "正在检查受影响的代理……", - "xpack.ingestManager.deletePackagePolicy.confirmModal.loadingButtonLabel": "正在加载……", - "xpack.ingestManager.deletePackagePolicy.failureMultipleNotificationTitle": "删除 {count} 个集成时出错", - "xpack.ingestManager.deletePackagePolicy.failureSingleNotificationTitle": "删除集成“{id}”时出错", - "xpack.ingestManager.deletePackagePolicy.fatalErrorNotificationTitle": "删除集成时出错", - "xpack.ingestManager.deletePackagePolicy.successMultipleNotificationTitle": "已删除 {count} 个集成", - "xpack.ingestManager.deletePackagePolicy.successSingleNotificationTitle": "已删除集成“{id}”", "xpack.ingestManager.disabledSecurityDescription": "必须在 Kibana 和 Elasticsearch 启用安全性,才能使用 Elastic Fleet。", "xpack.ingestManager.disabledSecurityTitle": "安全性未启用", - "xpack.ingestManager.editAgentPolicy.cancelButtonText": "取消", - "xpack.ingestManager.editAgentPolicy.errorNotificationTitle": "无法更新代理配置", - "xpack.ingestManager.editAgentPolicy.saveButtonText": "保存更改", - "xpack.ingestManager.editAgentPolicy.savingButtonText": "正在保存……", - "xpack.ingestManager.editAgentPolicy.successNotificationTitle": "已成功更新“{name}”设置", - "xpack.ingestManager.editAgentPolicy.unsavedChangesText": "您有未保存更改", - "xpack.ingestManager.editPackagePolicy.cancelButton": "取消", - "xpack.ingestManager.editPackagePolicy.errorLoadingDataMessage": "加载此集成信息时出错", - "xpack.ingestManager.editPackagePolicy.errorLoadingDataTitle": "加载数据时出错", - "xpack.ingestManager.editPackagePolicy.failedConflictNotificationMessage": "数据已过时。刷新页面以获取最新的配置。", - "xpack.ingestManager.editPackagePolicy.failedNotificationTitle": "更新“{packagePolicyName}”时出错", - "xpack.ingestManager.editPackagePolicy.pageDescription": "修改集成设置并将更改部署到选定代理配置。", - "xpack.ingestManager.editPackagePolicy.pageTitle": "编辑集成", - "xpack.ingestManager.editPackagePolicy.pageTitleWithPackageName": "编辑 {packageName} 集成", - "xpack.ingestManager.editPackagePolicy.saveButton": "保存集成", - "xpack.ingestManager.editPackagePolicy.updatedNotificationMessage": "Fleet 会将更新部署到所有使用配置“{agentPolicyName}”的代理", - "xpack.ingestManager.editPackagePolicy.updatedNotificationTitle": "已成功更新“{packagePolicyName}”", "xpack.ingestManager.enrollemntAPIKeyList.emptyMessage": "未找到任何注册令牌。", "xpack.ingestManager.enrollemntAPIKeyList.loadingTokensMessage": "正在加载注册令牌......", - "xpack.ingestManager.enrollmentStepAgentPolicy.policySelectAriaLabel": "代理配置", - "xpack.ingestManager.enrollmentStepAgentPolicy.policySelectLabel": "代理配置", - "xpack.ingestManager.enrollmentStepAgentPolicy.enrollmentTokenSelectLabel": "注册令牌", - "xpack.ingestManager.enrollmentStepAgentPolicy.showAuthenticationSettingsButton": "身份验证设置", + "xpack.ingestManager.enrollmentInstructions.descriptionText": "从代理的目录,运行相应命令以注册并启动 Elastic 代理。您可以重复使用这些命令在多台机器上设置代理。请务必以具有系统“管理员”权限的用户身份执行注册步骤。", + "xpack.ingestManager.enrollmentInstructions.linuxDebRpmTitle": "Linux(.deb 和 .rpm)", + "xpack.ingestManager.enrollmentInstructions.macLinuxTarInstructions": "如果代理的系统重新启动,您需要运行 {command}。", + "xpack.ingestManager.enrollmentInstructions.macLinuxTarTitle": "macOS/Linux (.tar.gz)", + "xpack.ingestManager.enrollmentInstructions.windowsTitle": "Windows", "xpack.ingestManager.enrollmentTokenDeleteModal.cancelButton": "取消", "xpack.ingestManager.enrollmentTokenDeleteModal.deleteButton": "删除", "xpack.ingestManager.enrollmentTokenDeleteModal.description": "确定要删除 {keyName}。", "xpack.ingestManager.enrollmentTokenDeleteModal.title": "删除注册令牌", "xpack.ingestManager.enrollmentTokensList.actionsTitle": "操作", "xpack.ingestManager.enrollmentTokensList.activeTitle": "活动", - "xpack.ingestManager.enrollmentTokensList.policyTitle": "代理配置", "xpack.ingestManager.enrollmentTokensList.createdAtTitle": "创建日期", + "xpack.ingestManager.enrollmentTokensList.hideTokenButtonLabel": "隐藏令牌", "xpack.ingestManager.enrollmentTokensList.nameTitle": "名称", "xpack.ingestManager.enrollmentTokensList.newKeyButton": "新建注册令牌", "xpack.ingestManager.enrollmentTokensList.pageDescription": "这是可用于注册代理的注册令牌列表。", + "xpack.ingestManager.enrollmentTokensList.revokeTokenButtonLabel": "撤销令牌", "xpack.ingestManager.enrollmentTokensList.secretTitle": "密钥", - "xpack.ingestManager.epm.addPackagePolicyButtonText": "添加 {packageName}", + "xpack.ingestManager.enrollmentTokensList.showTokenButtonLabel": "显示令牌", + "xpack.ingestManager.epm.assetGroupTitle": "{assetType} 资产", "xpack.ingestManager.epm.browseAllButtonText": "浏览所有集成", "xpack.ingestManager.epm.illustrationAltText": "集成的图示", "xpack.ingestManager.epm.loadingIntegrationErrorTitle": "加载集成详情时出错", "xpack.ingestManager.epm.packageDetailsNav.overviewLinkText": "概览", - "xpack.ingestManager.epm.packageDetailsNav.packagePoliciesLinkText": "使用情况", "xpack.ingestManager.epm.packageDetailsNav.settingsLinkText": "设置", "xpack.ingestManager.epm.pageSubtitle": "浏览集成以了解热门应用和服务。", "xpack.ingestManager.epm.pageTitle": "集成", @@ -9484,7 +9275,6 @@ "xpack.ingestManager.metadataForm.submitButtonText": "添加", "xpack.ingestManager.metadataForm.valueLabel": "值", "xpack.ingestManager.newEnrollmentKey.cancelButtonLabel": "取消", - "xpack.ingestManager.newEnrollmentKey.policyLabel": "配置", "xpack.ingestManager.newEnrollmentKey.flyoutTitle": "创建新的注册令牌", "xpack.ingestManager.newEnrollmentKey.keyCreatedToasts": "注册令牌已创建。", "xpack.ingestManager.newEnrollmentKey.nameLabel": "名称", @@ -9495,17 +9285,12 @@ "xpack.ingestManager.overviewAgentErrorTitle": "错误", "xpack.ingestManager.overviewAgentOfflineTitle": "脱机", "xpack.ingestManager.overviewAgentTotalTitle": "代理总数", - "xpack.ingestManager.overviewPolicyTotalTitle": "可用总计", "xpack.ingestManager.overviewDatastreamNamespacesTitle": "命名空间", "xpack.ingestManager.overviewDatastreamSizeTitle": "总大小", "xpack.ingestManager.overviewDatastreamTotalTitle": "数据集", "xpack.ingestManager.overviewIntegrationsInstalledTitle": "安装时间", "xpack.ingestManager.overviewIntegrationsTotalTitle": "可用总计", "xpack.ingestManager.overviewIntegrationsUpdatesAvailableTitle": "可用更新", - "xpack.ingestManager.overviewPackagePolicyTitle": "已配置集成", - "xpack.ingestManager.overviewPagePoliciesPanelAction": "查看配置", - "xpack.ingestManager.overviewPagePoliciesPanelTitle": "代理配置", - "xpack.ingestManager.overviewPagePoliciesPanelTooltip": "使用代理配置控制您的代理收集的数据。", "xpack.ingestManager.overviewPageDataStreamsPanelAction": "查看数据集", "xpack.ingestManager.overviewPageDataStreamsPanelTitle": "数据集", "xpack.ingestManager.overviewPageDataStreamsPanelTooltip": "您的代理收集的数据组织到各种数据集中。", @@ -9518,11 +9303,6 @@ "xpack.ingestManager.overviewPageIntegrationsPanelTooltip": "浏览并安装 Elastic Stack 的集成。将集成添加到您的代理配置以开始发送数据。", "xpack.ingestManager.overviewPageSubtitle": "Elastic 代理和代理配置的集中管理。", "xpack.ingestManager.overviewPageTitle": "Ingest Manager", - "xpack.ingestManager.packagePolicyValidation.invalidArrayErrorMessage": "格式无效", - "xpack.ingestManager.packagePolicyValidation.invalidYamlFormatErrorMessage": "YAML 格式无效", - "xpack.ingestManager.packagePolicyValidation.nameRequiredErrorMessage": "“名称”必填", - "xpack.ingestManager.packagePolicyValidation.namespaceRequiredErrorMessage": "命名空间必填", - "xpack.ingestManager.packagePolicyValidation.requiredErrorMessage": "“{fieldName}”必填", "xpack.ingestManager.permissionDeniedErrorMessage": "您无权访问 Ingest Manager。Ingest Manager 需要{roleName}权限。", "xpack.ingestManager.permissionDeniedErrorTitle": "权限被拒绝", "xpack.ingestManager.permissionsRequestErrorMessageDescription": "检查 Ingest Manager 权限时出现问题", @@ -9567,8 +9347,10 @@ "xpack.ingestManager.unenrollAgents.confirmModal.confirmButtonLabel": "取消注册", "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "取消注册 {count, plural, one {# 个代理} other {# 个代理}}?", "xpack.ingestManager.unenrollAgents.confirmModal.deleteSingleTitle": "取消注册“{id}”?", + "xpack.ingestManager.unenrollAgents.confirmModal.forceDeleteSingleTitle": "强制取消注册代理“{id}”?", "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "正在加载……", "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "取消注册代理时出错", + "xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle": "代理“{id}”已取消注册", "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "取消注册代理“{id}”", "xpack.ingestPipelines.app.checkingPrivilegesDescription": "正在检查权限……", "xpack.ingestPipelines.app.checkingPrivilegesErrorMessage": "从服务器获取用户权限时出错。", @@ -9692,13 +9474,12 @@ "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.cancelButtonLabel": "取消", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.confirmationButtonLabel": "删除处理器", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.titleText": "删除 {type} 处理器", - "xpack.ingestPipelines.pipelineEditor.setForm.fieldFieldLabel": "字段", - "xpack.ingestPipelines.pipelineEditor.setForm.fieldRequiredError": "字段值必填。", "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel": "覆盖", "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel": "值", "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "需要设置值。", "xpack.ingestPipelines.pipelineEditor.settingsForm.learnMoreLabelLink.processor": "{processorLabel}文档", "xpack.ingestPipelines.pipelineEditor.typeField.fieldRequiredError": "类型必填。", + "xpack.ingestPipelines.pipelineEditor.typeField.typeFieldComboboxPlaceholder": "键入后按“ENTER”键", "xpack.ingestPipelines.pipelineEditor.typeField.typeFieldLabel": "处理器", "xpack.ingestPipelines.processors.label.append": "追加", "xpack.ingestPipelines.processors.label.bytes": "字节", @@ -9708,7 +9489,7 @@ "xpack.ingestPipelines.processors.label.date": "日期", "xpack.ingestPipelines.processors.label.dateIndexName": "日期索引名称", "xpack.ingestPipelines.processors.label.dissect": "分解", - "xpack.ingestPipelines.processors.label.dotExpander": "点扩展器", + "xpack.ingestPipelines.processors.label.dotExpander": "点扩展", "xpack.ingestPipelines.processors.label.drop": "丢弃", "xpack.ingestPipelines.processors.label.enrich": "扩充", "xpack.ingestPipelines.processors.label.fail": "失败", @@ -9716,7 +9497,7 @@ "xpack.ingestPipelines.processors.label.geoip": "GeoIP", "xpack.ingestPipelines.processors.label.grok": "Grok", "xpack.ingestPipelines.processors.label.gsub": "Gsub", - "xpack.ingestPipelines.processors.label.htmlStrip": "HTML 剥离", + "xpack.ingestPipelines.processors.label.htmlStrip": "HTML 标记移除", "xpack.ingestPipelines.processors.label.inference": "推理", "xpack.ingestPipelines.processors.label.join": "联接", "xpack.ingestPipelines.processors.label.json": "JSON", @@ -9740,6 +9521,7 @@ "xpack.ingestPipelines.requestFlyout.unnamedTitle": "请求", "xpack.ingestPipelines.settingsFormOnFailureFlyout.addButtonLabel": "添加", "xpack.ingestPipelines.settingsFormOnFailureFlyout.cancelButtonLabel": "取消", + "xpack.ingestPipelines.settingsFormOnFailureFlyout.updateButtonLabel": "更新", "xpack.ingestPipelines.tabs.outputTabTitle": "输出", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel": "文档", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError": "文档 JSON 无效。", @@ -9779,7 +9561,6 @@ "xpack.lens.configure.editConfig": "编辑配置", "xpack.lens.configure.emptyConfig": "将字段拖放到此处", "xpack.lens.dataPanelWrapper.switchDatasource": "切换到数据源", - "xpack.lens.datatable.columns": "字段", "xpack.lens.datatable.conjunctionSign": " & ", "xpack.lens.datatable.expressionHelpLabel": "数据表呈现器", "xpack.lens.datatable.label": "数据表", @@ -9825,6 +9606,7 @@ "xpack.lens.functions.renameColumns.idMap.help": "旧列 ID 为键且相应新列 ID 为值的 JSON 编码对象。所有其他列 ID 都将保留。", "xpack.lens.includeValueButtonAriaLabel": "包括 {value}", "xpack.lens.includeValueButtonTooltip": "包括值", + "xpack.lens.indexPattern.allFieldsLabel": "所有字段", "xpack.lens.indexPattern.availableFieldsLabel": "可用字段", "xpack.lens.indexPattern.avg": "平均值", "xpack.lens.indexPattern.avgOf": "{name} 的平均值", @@ -9852,6 +9634,8 @@ "xpack.lens.indexPattern.defaultFormatLabel": "默认值", "xpack.lens.indexPattern.emptyFieldsLabel": "空字段", "xpack.lens.indexpattern.emptyTextColumnValue": "(空)", + "xpack.lens.indexPattern.existenceErrorAriaLabel": "现有内容提取失败", + "xpack.lens.indexPattern.existenceErrorLabel": "无法加载字段信息", "xpack.lens.indexPattern.fieldDistributionLabel": "分布", "xpack.lens.indexPattern.fieldItemTooltip": "拖放以可视化。", "xpack.lens.indexPattern.fieldlessOperationLabel": "要使用此函数,请选择字段。", @@ -9860,6 +9644,7 @@ "xpack.lens.indexPattern.fieldStatsButtonLabel": "单击以进行字段预览,或拖放以进行可视化。", "xpack.lens.indexPattern.fieldStatsCountLabel": "计数", "xpack.lens.indexPattern.fieldStatsDisplayToggle": "切换", + "xpack.lens.indexPattern.fieldStatsNoData": "没有可显示的数据", "xpack.lens.indexPattern.fieldTimeDistributionLabel": "时间分布", "xpack.lens.indexPattern.fieldTopValuesLabel": "排名最前值", "xpack.lens.indexPattern.groupByDropdown": "分组依据", @@ -10166,7 +9951,7 @@ "xpack.maps.aggs.defaultCountLabel": "计数", "xpack.maps.appTitle": "Maps", "xpack.maps.blendedVectorLayer.clusteredLayerName": "集群 {displayName}", - "xpack.maps.breadCrumbs.unsavedChangesWarning": "可能不会保存您未保存的更改", + "xpack.maps.breadCrumbs.unsavedChangesWarning": "您的地图中包含未保存的更改。是否确定要离开?", "xpack.maps.choropleth.boundaries.elasticsearch": "Elasticsearch 的点、线和多边形", "xpack.maps.choropleth.boundaries.ems": "来自 Elastic Maps Service 的管理边界", "xpack.maps.choropleth.boundariesLabel": "边界源", @@ -10495,12 +10280,12 @@ "xpack.maps.source.kbnTMSDescription": "在 kibana.yml 中配置的地图磁贴", "xpack.maps.source.kbnTMSTitle": "定制磁贴地图服务", "xpack.maps.source.mapSettingsPanel.initialLocationLabel": "初始地图位置", - "xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle": ".pbf 矢量磁贴", + "xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle": "矢量磁贴", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "缩放", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsMessage": "字段", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsPostHelpMessage": "这可以用于工具提示和动态样式。", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsPreHelpMessage": "可用的字段,于 ", - "xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "磁帖图层", + "xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "源图层", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlHelpMessage": ".mvt 矢量磁帖服务的 URL。例如 {url}", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlMessage": "URL", "xpack.maps.source.MVTSingleLayerVectorSourceEditor.zoomRangeHelpMessage": "磁帖中存在该图层的缩放级别。这不直接对应于可见性。较低级别的图层数据始终可以显示在较高缩放级别(反之却不然)。", @@ -10637,7 +10422,9 @@ "xpack.maps.visTypeAlias.legacyMapVizWarning": "使用 Maps 应用而非坐标地图和区域地图。\nMaps 应用提供更多的功能并更加易用。", "xpack.maps.visTypeAlias.title": "Maps", "xpack.maps.xyztmssource.attributionLink": "属性文本必须附带链接", + "xpack.maps.xyztmssource.attributionLinkLabel": "属性链接", "xpack.maps.xyztmssource.attributionText": "属性 url 必须附带文本", + "xpack.maps.xyztmssource.attributionTextLabel": "属性文本", "xpack.ml.accessDenied.description": "您无权访问 ML 插件", "xpack.ml.accessDenied.label": "权限不足", "xpack.ml.accessDeniedLabel": "访问被拒绝", @@ -10913,7 +10700,7 @@ "xpack.ml.dataframe.analytics.create.calloutMessage": "加载分析字段的其他数据。", "xpack.ml.dataframe.analytics.create.calloutTitle": "分析字段不可用", "xpack.ml.dataframe.analytics.create.chooseSourceTitle": "选择源索引模式", - "xpack.ml.dataframe.analytics.create.classificationHelpText": "分类作业需要映射为表状数据结构的源索引,并支持数值、布尔、文本、关键字或 IP 字段。使用高级编辑器来应用定制选项,如预测字段名称。", + "xpack.ml.dataframe.analytics.create.classificationHelpText": "分类用于预测数据集中的数据点的标签。", "xpack.ml.dataframe.analytics.create.computeFeatureInfluenceFalseValue": "False", "xpack.ml.dataframe.analytics.create.computeFeatureInfluenceLabel": "计算功能影响", "xpack.ml.dataframe.analytics.create.computeFeatureInfluenceLabelHelpText": "指定是否启用功能影响计算。默认为 true。", @@ -10962,6 +10749,7 @@ "xpack.ml.dataframe.analytics.create.detailsDetails.editButtonText": "编辑", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessage": "创建 Kibana 索引模式时发生错误:", "xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError": "索引模式 {indexPatternName} 已存在。", + "xpack.ml.dataframe.analytics.create.errorCheckingIndexExists": "获取现有索引名称时发生以下错误:{error}", "xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob": "创建数据帧分析作业时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList": "获取现有数据帧分析作业 ID 时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", @@ -11022,11 +10810,11 @@ "xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesHelpText": "指定要返回的每文档功能重要性值最大数目。", "xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesInputAriaLabel": "每文档功能重要性值最大数目。", "xpack.ml.dataframe.analytics.create.numTopFeatureImportanceValuesLabel": "功能重要性值", - "xpack.ml.dataframe.analytics.create.outlierDetectionHelpText": "离群值检测作业需要映射为表状数据结构的源索引,并仅分析数值和布尔值字段。使用高级编辑器将定制选项添加到配置。", + "xpack.ml.dataframe.analytics.create.outlierDetectionHelpText": "异常值检测用于识别数据集中的异常数据点。", "xpack.ml.dataframe.analytics.create.outlierFractionHelpText": "设置在离群值检测之前被假设为离群的数据集比例。", "xpack.ml.dataframe.analytics.create.outlierFractionInputAriaLabel": "设置在离群值检测之前被假设为离群的数据集比例。", "xpack.ml.dataframe.analytics.create.outlierFractionLabel": "离群值比例", - "xpack.ml.dataframe.analytics.create.outlierRegressionHelpText": "回归作业仅分析数值字段。使用高级编辑器来应用定制选项,如预测字段名称。", + "xpack.ml.dataframe.analytics.create.outlierRegressionHelpText": "回归用于预测数据集中的数值。", "xpack.ml.dataframe.analytics.create.predictionFieldNameHelpText": "定义结果中预测字段的名称。默认为 _prediction。", "xpack.ml.dataframe.analytics.create.predictionFieldNameLabel": "预测字段名称", "xpack.ml.dataframe.analytics.create.randomizeSeedInputAriaLabel": "用于选取哪个文档用于训练的随机生成器种子", @@ -11105,6 +10893,7 @@ "xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle": "训练误差", "xpack.ml.dataframe.analytics.regressionExploration.trainingFilterText": ".筛留测试数据。", "xpack.ml.dataframe.analyticsList.analyticsDetails.tabs.analyticsMessagesLabel": "作业消息", + "xpack.ml.dataframe.analyticsList.cloneActionPermissionTooltip": "您无权克隆分析作业。", "xpack.ml.dataframe.analyticsList.completeBatchAnalyticsToolTip": "{analyticsId} 为已完成的分析作业,无法重新启动。", "xpack.ml.dataframe.analyticsList.createDataFrameAnalyticsButton": "创建作业", "xpack.ml.dataframe.analyticsList.deleteActionDisabledToolTipContent": "停止数据帧分析作业,以便将其删除。", @@ -11175,7 +10964,11 @@ "xpack.ml.dataframe.analyticsList.title": "数据帧分析作业", "xpack.ml.dataframe.analyticsList.type": "类型", "xpack.ml.dataframe.analyticsList.typeFilter": "类型", + "xpack.ml.dataframe.analyticsList.viewActionJobFailedToolTipContent": "数据帧分析作业失败。没有可用的结果页面。", + "xpack.ml.dataframe.analyticsList.viewActionJobNotFinishedToolTipContent": "未完成数据帧分析作业。没有可用的结果页面。", + "xpack.ml.dataframe.analyticsList.viewActionJobNotStartedToolTipContent": "数据帧分析作业尚未启动。没有可用的结果页面。", "xpack.ml.dataframe.analyticsList.viewActionName": "查看", + "xpack.ml.dataframe.analyticsList.viewActionUnknownJobTypeToolTipContent": "没有可用于此类型数据帧分析作业的结果页面。", "xpack.ml.dataframe.stepCreateForm.createDataFrameAnalyticsSuccessMessage": "数据帧分析 {jobId} 创建请求已确认。", "xpack.ml.dataframe.stepDetailsForm.destinationIndexInvalidErrorLink": "详细了解索引名称限制。", "xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel": "探查", @@ -11254,6 +11047,8 @@ "xpack.ml.explorer.addToDashboard.selectDashboardsLabel": "选择仪表板:", "xpack.ml.explorer.addToDashboard.selectSwimlanesLabel": "选择泳道视图:", "xpack.ml.explorer.addToDashboardLabel": "添加到仪表板", + "xpack.ml.explorer.annotationsErrorCallOutTitle": "加载注释时发生错误:", + "xpack.ml.explorer.annotationsErrorTitle": "注释", "xpack.ml.explorer.annotationsTitle": "标注 {badge}", "xpack.ml.explorer.annotationsTitleTotalCount": "总计:{count}", "xpack.ml.explorer.anomaliesTitle": "异常", @@ -11383,7 +11178,7 @@ "xpack.ml.fileDatavisualizer.analysisSummary.timeFormatTitle": "时间 {timestampFormats, plural, zero {格式} one {format} 其他 {formats}}", "xpack.ml.fileDatavisualizer.bottomBar.backButtonLabel": "上一步", "xpack.ml.fileDatavisualizer.bottomBar.cancelButtonLabel": "取消", - "xpack.ml.fileDatavisualizer.bottomBar.missingImportPrivilegesMessage": "您没有所需的权限,无法导入数据", + "xpack.ml.fileDatavisualizer.bottomBar.missingImportPrivilegesMessage": "您需要具有 ingest_admin 角色才能启用数据导入", "xpack.ml.fileDatavisualizer.bottomBar.readMode.cancelButtonLabel": "取消", "xpack.ml.fileDatavisualizer.bottomBar.readMode.importButtonLabel": "导入", "xpack.ml.fileDatavisualizer.editFlyout.applyOverrideSettingsButtonLabel": "应用", @@ -11900,6 +11695,7 @@ "xpack.ml.navMenu.overviewTabLinkText": "概览", "xpack.ml.navMenu.settingsTabLinkText": "设置", "xpack.ml.newJob.page.createJob": "创建作业", + "xpack.ml.newJob.page.createJob.indexPatternTitle": "使用索引模式 {index}", "xpack.ml.newJob.recognize.advancedLabel": "高级", "xpack.ml.newJob.recognize.advancedSettingsAriaLabel": "高级设置", "xpack.ml.newJob.recognize.alreadyExistsLabel": "(已存在)", @@ -12441,6 +12237,8 @@ "xpack.ml.timeSeriesExplorer.annotationFlyout.maxLengthError": "超过最大长度 ({maxChars} 个字符) {charsOver, number} {charsOver, plural, one {个字符} other {}}", "xpack.ml.timeSeriesExplorer.annotationFlyout.noAnnotationTextError": "输入注释文本", "xpack.ml.timeSeriesExplorer.annotationFlyout.updateButtonLabel": "更新", + "xpack.ml.timeSeriesExplorer.annotationsErrorCallOutTitle": "加载注释时发生错误:", + "xpack.ml.timeSeriesExplorer.annotationsErrorTitle": "注释", "xpack.ml.timeSeriesExplorer.annotationsLabel": "注释", "xpack.ml.timeSeriesExplorer.annotationsTitle": "标注 {badge}", "xpack.ml.timeSeriesExplorer.anomaliesTitle": "异常", @@ -12695,6 +12493,7 @@ "xpack.monitoring.alerts.nodesChanged.resolved.internalShortMessage": "已为 {clusterName} 解决 Elasticsearch 节点已更改告警。", "xpack.monitoring.alerts.nodesChanged.shortAction": "确认您已添加、移除或重新启动节点。", "xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage": "Elasticsearch 节点“{added}”已添加到此集群。", + "xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage": "Elasticsearch 节点已更改", "xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage": "Elasticsearch 节点“{removed}”已从此集群中移除。", "xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage": "此集群的 Elasticsearch 节点中没有更改。", "xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage": "此集群中 Elasticsearch 节点“{restarted}”已重新启动。", @@ -12976,6 +12775,7 @@ "xpack.monitoring.elasticsearch.node.advanced.routeTitle": "Elasticsearch - 节点 - {nodeSummaryName} - 高级", "xpack.monitoring.elasticsearch.node.overview.routeTitle": "Elasticsearch - 节点 - {nodeName} - 概览", "xpack.monitoring.elasticsearch.node.statusIconLabel": "状态:{status}", + "xpack.monitoring.elasticsearch.nodeDetailStatus.alerts": "告警", "xpack.monitoring.elasticsearch.nodeDetailStatus.dataLabel": "数据", "xpack.monitoring.elasticsearch.nodeDetailStatus.documentsLabel": "文档", "xpack.monitoring.elasticsearch.nodeDetailStatus.freeDiskSpaceLabel": "可用磁盘空间", @@ -13078,6 +12878,9 @@ "xpack.monitoring.febLabel": "二月", "xpack.monitoring.formatNumbers.notAvailableLabel": "不适用", "xpack.monitoring.friLabel": "周五", + "xpack.monitoring.healthCheck.encryptionErrorAction": "了解操作方法。", + "xpack.monitoring.healthCheck.tlsAndEncryptionError": "必须在 Kibana 与 Elasticsearch 之间启用传输层安全, \n 并在 kibana.yml 文件中配置加密密钥,才能使用 Alerting 功能。", + "xpack.monitoring.healthCheck.tlsAndEncryptionErrorTitle": "需要其他设置", "xpack.monitoring.janLabel": "一月", "xpack.monitoring.julLabel": "七月", "xpack.monitoring.junLabel": "六月", @@ -13940,7 +13743,7 @@ "xpack.observability.emptySection.apps.logs.link": "安装 Filebeat", "xpack.observability.emptySection.apps.logs.title": "日志", "xpack.observability.emptySection.apps.metrics.description": "分析您的基础设施、应用和服务的指标。发现趋势、预测行为、接收异常告警等等。", - "xpack.observability.emptySection.apps.metrics.link": "安装指标模块", + "xpack.observability.emptySection.apps.metrics.link": "安装 Metricbeat", "xpack.observability.emptySection.apps.metrics.title": "指标", "xpack.observability.emptySection.apps.uptime.description": "主动监测站点和服务的可用性。接收告警并更快地解决问题,从而优化用户体验。", "xpack.observability.emptySection.apps.uptime.link": "安装 Heartbeat", @@ -13952,6 +13755,10 @@ "xpack.observability.home.sectionsubtitle": "通过根据需要将日志、指标和跟踪都置于单个堆栈上,来监测、分析和响应环境中任何位置发生的事件。", "xpack.observability.home.sectionTitle": "整个生态系统的统一可见性", "xpack.observability.home.title": "可观测性", + "xpack.observability.ingestManager.beta": "公测版", + "xpack.observability.ingestManager.button": "试用采集管理器公测版", + "xpack.observability.ingestManager.text": "通过 Elastic 代理,可以简单统一的方式将日志、指标和其他类型数据的监测添加到主机。不再需要安装多个 Beats 和其他代理,这简化和加快了将配置部署到整个基础设施的过程。", + "xpack.observability.ingestManager.title": "是否见过我们的新型采集管理器?", "xpack.observability.landing.breadcrumb": "入门", "xpack.observability.news.readFullStory": "详细了解", "xpack.observability.news.title": "最近的新闻", @@ -15013,7 +14820,7 @@ "xpack.securitySolution.add_filter_to_global_search_bar.filterOutValueHoverAction": "筛除值", "xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel": "选择此规则生成的所有告警的风险分数。", "xpack.securitySolution.alerts.riskScoreMapping.defaultRiskScoreTitle": "默认风险分数", - "xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel": "将字段从源事件(刻度值 1-100)映射到风险分数。", + "xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel": "使用源事件值覆盖默认风险分数。", "xpack.securitySolution.alerts.riskScoreMapping.mappingDetailsLabel": "如果值超出范围,或字段不存在,将使用默认风险分数。", "xpack.securitySolution.alerts.riskScoreMapping.riskScoreFieldTitle": "signal.rule.risk_score", "xpack.securitySolution.alerts.riskScoreMapping.riskScoreMappingTitle": "风险分数覆盖", @@ -15021,7 +14828,7 @@ "xpack.securitySolution.alerts.riskScoreMapping.sourceFieldTitle": "源字段", "xpack.securitySolution.alerts.severityMapping.defaultDescriptionLabel": "选择此规则生成的所有告警的严重性级别。", "xpack.securitySolution.alerts.severityMapping.defaultSeverityTitle": "严重性", - "xpack.securitySolution.alerts.severityMapping.mappingDescriptionLabel": "将值从源事件映射到特定严重性。", + "xpack.securitySolution.alerts.severityMapping.mappingDescriptionLabel": "使用源事件值覆盖默认严重性。", "xpack.securitySolution.alerts.severityMapping.mappingDetailsLabel": "对于多匹配,最高严重性匹配将适用。如果未找到匹配,将使用默认严重性。", "xpack.securitySolution.alerts.severityMapping.severityMappingTitle": "严重性覆盖", "xpack.securitySolution.alerts.severityMapping.severityTitle": "默认严重性", @@ -15354,11 +15161,11 @@ "xpack.securitySolution.case.connectors.resilient.actionTypeTitle": "IBM Resilient", "xpack.securitySolution.case.connectors.resilient.apiKeyId": "API 密钥 ID", "xpack.securitySolution.case.connectors.resilient.apiKeySecret": "API 密钥密码", - "xpack.securitySolution.case.connectors.resilient.orgId": "组织 Id", + "xpack.securitySolution.case.connectors.resilient.orgId": "组织 ID", "xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField": "“API 密钥 ID”必填", "xpack.securitySolution.case.connectors.resilient.requiredApiKeySecretTextField": "“API 密钥密码”必填", - "xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField": "组织 Id", - "xpack.securitySolution.case.connectors.resilient.selectMessageText": "将 SIEM 案例数据推送或更新到 Resilient 中的新问题", + "xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField": "“组织 ID”必填", + "xpack.securitySolution.case.connectors.resilient.selectMessageText": "将 Security 案例数据推送或更新到 Resilient 中的新问题", "xpack.securitySolution.case.createCase.descriptionFieldRequiredError": "描述必填。", "xpack.securitySolution.case.createCase.fieldTagsHelpText": "为此案例键入一个或多个定制识别标记。在每个标记后按 Enter 键可开始新的标记。", "xpack.securitySolution.case.createCase.titleFieldRequiredError": "标题必填。", @@ -15371,8 +15178,8 @@ "xpack.securitySolution.chart.allOthersGroupingLabel": "所有其他", "xpack.securitySolution.chart.dataAllValuesZerosTitle": "所有值返回了零", "xpack.securitySolution.chart.dataNotAvailableTitle": "图表数据不可用", - "xpack.securitySolution.chrome.help.appName": "SIEM", - "xpack.securitySolution.chrome.helpMenu.documentation": "SIEM 文档", + "xpack.securitySolution.chrome.help.appName": "Security", + "xpack.securitySolution.chrome.helpMenu.documentation": "Security 文档", "xpack.securitySolution.chrome.helpMenu.documentation.ecs": "ECS 文档", "xpack.securitySolution.clipboard.copied": "已复制", "xpack.securitySolution.clipboard.copy": "复制", @@ -15432,7 +15239,7 @@ "xpack.securitySolution.components.mlPopup.errors.createJobFailureTitle": "创建作业失败", "xpack.securitySolution.components.mlPopup.errors.startJobFailureTitle": "启动作业失败", "xpack.securitySolution.components.mlPopup.hooks.errors.indexPatternFetchFailureTitle": "索引模式提取失败", - "xpack.securitySolution.components.mlPopup.hooks.errors.siemJobFetchFailureTitle": "SIEM 作业提取失败", + "xpack.securitySolution.components.mlPopup.hooks.errors.siemJobFetchFailureTitle": "Security 作业提取失败", "xpack.securitySolution.components.mlPopup.jobsTable.createCustomJobButtonLabel": "创建定制作业", "xpack.securitySolution.components.mlPopup.jobsTable.jobNameColumn": "作业名称", "xpack.securitySolution.components.mlPopup.jobsTable.noItemsDescription": "未找到任何 SIEM Machine Learning 作业", @@ -15510,7 +15317,7 @@ "xpack.securitySolution.dataProviders.valueAriaLabel": "值", "xpack.securitySolution.dataProviders.valuePlaceholder": "值", "xpack.securitySolution.detectionEngine.alerts.actions.addEndpointException": "添加终端例外", - "xpack.securitySolution.detectionEngine.alerts.actions.addException": "添加例外", + "xpack.securitySolution.detectionEngine.alerts.actions.addException": "添加规则例外", "xpack.securitySolution.detectionEngine.alerts.actions.closeAlertTitle": "关闭告警", "xpack.securitySolution.detectionEngine.alerts.actions.inProgressAlertTitle": "标记为进行中", "xpack.securitySolution.detectionEngine.alerts.actions.investigateInTimelineTitle": "在时间线中调查", @@ -15557,7 +15364,7 @@ "xpack.securitySolution.detectionEngine.alerts.utilityBar.selectedAlertsTitle": "已选择 {selectedAlertsFormatted} 个{selectedAlerts, plural, =1 {告警} other {告警}}", "xpack.securitySolution.detectionEngine.alerts.utilityBar.showingAlertsTitle": "正在显示 {totalAlertsFormatted} 个{totalAlerts, plural, =1 {告警} other {告警}}", "xpack.securitySolution.detectionEngine.alerts.utilityBar.takeActionTitle": "采取操作", - "xpack.securitySolution.detectionEngine.alertTitle": "外部告警", + "xpack.securitySolution.detectionEngine.alertTitle": "检测告警", "xpack.securitySolution.detectionEngine.buttonManageRules": "管理检测规则", "xpack.securitySolution.detectionEngine.components.importRuleModal.cancelTitle": "取消", "xpack.securitySolution.detectionEngine.components.importRuleModal.importFailedDetailedTitle": "规则 ID:{ruleId}\n 状态代码:{statusCode}\n 消息:{message}", @@ -15580,7 +15387,7 @@ "xpack.securitySolution.detectionEngine.createRule.savedIdLabel": "已保存查询名称", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.descriptionFieldRequiredError": "描述必填。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fiedIndexPatternsLabel": "索引模式", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAssociatedToEndpointListLabel": "将规则关联到全局终端异常列表", + "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAssociatedToEndpointListLabel": "将现有的终端例外添加到规则", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAuthorHelpText": "为此规则键入一个或多个作者。键入每个作者后按 Enter 键以添加新作者。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAuthorLabel": "作者", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldBuildingBlockLabel": "将所有生成的告警标记为“构建块”告警", @@ -15602,7 +15409,7 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel": "时间线模板", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimestampOverrideHelpText": "选择执行规则时使用的时间戳字段。选取时间戳最接近于采集时间的字段(例如 event.ingested)。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimestampOverrideLabel": "时间戳覆盖", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideHelpText": "为执行信号调查的分析师提供有用信息。此指南将显示在规则详情页面上以及从此规则所生成的告警创建的时间线中。", + "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideHelpText": "为正在调查检测告警的分析人员提供有用的信息。本指南(作为备注)将显示在规则详情页面上以及从此规则所生成的检测告警创建的时间线中。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel": "调查指南", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名称必填。", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "添加规则调查指南......", @@ -15610,7 +15417,7 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addReferenceDescription": "添加引用 URL", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.advancedSettingsButton": "高级设置", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.buildingBlockLabel": "构建块", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.endpointExceptionListLabel": "全局终端异常列表", + "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.endpointExceptionListLabel": "Elastic 终端例外", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionCriticalDescription": "紧急", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionHighDescription": "高", "xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.severityOptionLowDescription": "低", @@ -15628,7 +15435,7 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesCustomDescription": "提供定制的索引列表", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesFromConfigDescription": "使用 Security Solution 高级设置中的 Elasticsearch 索引", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.indicesHelperDescription": "输入要运行此规则的 Elasticsearch 索引的模式。默认情况下,将包括 Security Solution 高级设置中定义的索引模式。", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdHelpText": "我们提供若干帮助您入门的常规作业。要添加自己的定制规则,请在 {machineLearning} 应用程序中将一组“siem”分配给这些作业,以使它们显示在此处。", + "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdHelpText": "我们提供了一些常见作业来帮助您入门。要添加自己的定制规则,请在 {machineLearning} 应用程序中将一组“security”分配给这些作业,以使其显示在此处。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired": "Machine Learning 作业必填。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.mlEnableJobWarningTitle": "此 ML 作业当前未运行。在激活此规则之前请通过“ML 作业设置”设置此作业以使其运行。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.mlJobSelectPlaceholderText": "选择作业", @@ -15661,6 +15468,7 @@ "xpack.securitySolution.detectionEngine.details.stepAboutRule.aboutText": "关于", "xpack.securitySolution.detectionEngine.details.stepAboutRule.detailsLabel": "详情", "xpack.securitySolution.detectionEngine.details.stepAboutRule.investigationGuideLabel": "调查指南", + "xpack.securitySolution.detectionEngine.detectionsBreadcrumbTitle": "检测", "xpack.securitySolution.detectionEngine.detectionsPageTitle": "检测告警", "xpack.securitySolution.detectionEngine.dismissButton": "关闭", "xpack.securitySolution.detectionEngine.dismissNoApiIntegrationKeyButton": "关闭", @@ -15670,6 +15478,7 @@ "xpack.securitySolution.detectionEngine.editRule.errorMsgDescription": "抱歉", "xpack.securitySolution.detectionEngine.editRule.pageTitle": "编辑规则设置", "xpack.securitySolution.detectionEngine.editRule.saveChangeTitle": "保存更改", + "xpack.securitySolution.detectionEngine.emptyActionBeats": "查看设置说明", "xpack.securitySolution.detectionEngine.emptyActionSecondary": "前往文档", "xpack.securitySolution.detectionEngine.emptyTitle": "似乎在 Security 应用程序中没有与检测引擎相关的索引", "xpack.securitySolution.detectionEngine.goToDocumentationButton": "查看文档", @@ -15961,6 +15770,10 @@ "xpack.securitySolution.detectionEngine.mitreAttackTechniques.xslScriptProcessingDescription": "XSL Script Processing (T1220)", "xpack.securitySolution.detectionEngine.mlRulesDisabledMessageTitle": "ML 规则需要白金级许可证以及 ML 管理员权限", "xpack.securitySolution.detectionEngine.mlUnavailableTitle": "{totalRules} 个{totalRules, plural, =1 {规则需要} other {规则需要}}启用 Machine Learning。", + "xpack.securitySolution.detectionEngine.needsIndexPermissionsMessage": "要使用检测引擎,具有所需集群和索引权限的用户必须首先访问此页面。{additionalContext}如欲获得更多帮助,请联系 Elastic Stack 管理员。", + "xpack.securitySolution.detectionEngine.needsListsIndexesMessage": "您需要具有列表索引的权限。", + "xpack.securitySolution.detectionEngine.needsSignalsAndListsIndexesMessage": "您需要具有信号索引和列表索引的权限。", + "xpack.securitySolution.detectionEngine.needsSignalsIndexMessage": "您需要具有信号索引的权限。", "xpack.securitySolution.detectionEngine.noApiIntegrationKeyCallOutMsg": "每次启动 Kibana,都会为已保存对象生成新的加密密钥。没有持久性密钥,在 Kibana 重新启动后,将无法删除或修改规则。要设置持久性密钥,请将文本值为 32 个或更多任意字符的 xpack.encryptedSavedObjects.encryptionKey 设置添加到 kibana.yml 文件。", "xpack.securitySolution.detectionEngine.noApiIntegrationKeyCallOutTitle": "需要 API 集成密钥", "xpack.securitySolution.detectionEngine.noIndexTitle": "让我们来设置您的检测引擎", @@ -16052,9 +15865,9 @@ "xpack.securitySolution.detectionEngine.rules.optionalFieldDescription": "可选", "xpack.securitySolution.detectionEngine.rules.pageTitle": "检测规则", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.createOwnRuletButton": "创建自己的规则", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elastic Security 提供预构建检测规则,它们运行在后台并在条件满足时创建告警。默认情况下,所有预构建规则处于禁用状态,请选择您要激活的规则。", + "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elastic Security 附带预置检测规则,这些规则在后台运行,并在条件满足时创建告警。默认情况下,除 Elastic Endpoint Security 规则外,所有预置规则都处于禁用状态。您可以选择其他要激活的规则。", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptTitle": "加载 Elastic 预构建检测规则", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "加载预构建检测规则", + "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "加载预置检测规则和时间线模板", "xpack.securitySolution.detectionEngine.rules.releaseNotesHelp": "发行说明", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton": "安装 {missingRules} 个 Elastic 预构建{missingRules, plural, =1 {规则} other {规则}}以及 {missingTimelines} 个 Elastic 预构建{missingTimelines, plural, =1 {时间线} other {时间线}} ", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton": "安装 {missingRules} 个 Elastic 预构建{missingRules, plural, =1 {规则} other {规则}} ", @@ -16083,6 +15896,7 @@ "xpack.securitySolution.detectionEngine.totalSignalTitle": "合计", "xpack.securitySolution.detectionEngine.userUnauthenticatedMsgBody": "您没有所需的权限,无法查看检测引擎。若需要更多帮助,请联系您的管理员。", "xpack.securitySolution.detectionEngine.userUnauthenticatedTitle": "需要检测引擎权限", + "xpack.securitySolution.detectionEngine.validations.thresholdValueFieldData.numberGreaterThanOrEqualOneErrorMessage": "值必须大于或等于 1。", "xpack.securitySolution.dragAndDrop.addToTimeline": "添加到时间线调查", "xpack.securitySolution.dragAndDrop.closeButtonLabel": "关闭", "xpack.securitySolution.dragAndDrop.copyToClipboardTooltip": "复制到剪贴板", @@ -16106,70 +15920,13 @@ "xpack.securitySolution.editDataProvider.valuePlaceholder": "值", "xpack.securitySolution.emptyMessage": "Elastic Security 将免费且开放的 Elastic SIEM 和 Elastic Endpoint Security 整合在一起,从而防御、检测并响应威胁。首先,您需要将安全解决方案相关数据添加到 Elastic Stack。有关更多信息,请查看我们的 ", "xpack.securitySolution.emptyString.emptyStringDescription": "空字符串", - "xpack.securitySolution.endpoint.details.endpointVersion": "Endpoint 版本", - "xpack.securitySolution.endpoint.details.errorBody": "请退出浮出控件并选择可用主机。", - "xpack.securitySolution.endpoint.details.errorTitle": "找不到主机", - "xpack.securitySolution.endpoint.details.hostname": "主机名", - "xpack.securitySolution.endpoint.details.ipAddress": "IP 地址", - "xpack.securitySolution.endpoint.details.lastSeen": "最后看到时间", - "xpack.securitySolution.endpoint.details.linkToIngestTitle": "重新分配策略", - "xpack.securitySolution.endpoint.details.os": "OS", - "xpack.securitySolution.endpoint.details.policy": "政策", - "xpack.securitySolution.endpoint.details.policyStatus": "策略状态", - "xpack.securitySolution.endpoint.details.policyStatusValue": "{policyStatus, select, success {成功} warning {警告} failure {失败} other {未知}}", - "xpack.securitySolution.endpoint.policyResponse.backLinkTitle": "终端详情", - "xpack.securitySolution.endpoint.policyResponse.title": "策略响应", - "xpack.securitySolution.endpoint.details.noPolicyResponse": "没有可用的策略响应", - "xpack.securitySolution.endpoint.details.policyResponse.configure_dns_events": "配置 DNS 事件", - "xpack.securitySolution.endpoint.details.policyResponse.configure_elasticsearch_connection": "配置 Elastic 搜索连接", - "xpack.securitySolution.endpoint.details.policyResponse.configure_file_events": "配置文件事件", - "xpack.securitySolution.endpoint.details.policyResponse.configure_imageload_events": "配置映像加载事件", - "xpack.securitySolution.endpoint.details.policyResponse.configure_kernel": "配置内核", - "xpack.securitySolution.endpoint.details.policyResponse.configure_logging": "配置日志记录", - "xpack.securitySolution.endpoint.details.policyResponse.configure_malware": "配置恶意软件", - "xpack.securitySolution.endpoint.details.policyResponse.configure_network_events": "配置网络事件", - "xpack.securitySolution.endpoint.details.policyResponse.configure_process_events": "配置进程事件", - "xpack.securitySolution.endpoint.details.policyResponse.configure_registry_events": "配置注册表事件", - "xpack.securitySolution.endpoint.details.policyResponse.configure_security_events": "配置安全事件", - "xpack.securitySolution.endpoint.details.policyResponse.connect_kernel": "连接内核", - "xpack.securitySolution.endpoint.details.policyResponse.detect_async_image_load_events": "检测异步映像加载事件", - "xpack.securitySolution.endpoint.details.policyResponse.detect_file_open_events": "检测文件打开事件", - "xpack.securitySolution.endpoint.details.policyResponse.detect_file_write_events": "检测文件写入事件", - "xpack.securitySolution.endpoint.details.policyResponse.detect_network_events": "检测网络事件", - "xpack.securitySolution.endpoint.details.policyResponse.detect_process_events": "检测进程事件", - "xpack.securitySolution.endpoint.details.policyResponse.detect_registry_events": "检测注册表事件", - "xpack.securitySolution.endpoint.details.policyResponse.detect_sync_image_load_events": "检测同步映像加载事件", - "xpack.securitySolution.endpoint.details.policyResponse.download_global_artifacts": "下载全局项目", - "xpack.securitySolution.endpoint.details.policyResponse.download_user_artifacts": "下面用户项目", - "xpack.securitySolution.endpoint.details.policyResponse.events": "事件", - "xpack.securitySolution.endpoint.details.policyResponse.failed": "失败", - "xpack.securitySolution.endpoint.details.policyResponse.load_config": "加载配置", - "xpack.securitySolution.endpoint.details.policyResponse.load_malware_model": "加载恶意软件模型", - "xpack.securitySolution.endpoint.details.policyResponse.logging": "日志", - "xpack.securitySolution.endpoint.details.policyResponse.malware": "恶意软件", - "xpack.securitySolution.endpoint.details.policyResponse.read_elasticsearch_config": "读取 ElasticSearch 配置", - "xpack.securitySolution.endpoint.details.policyResponse.read_events_config": "读取时间配置", - "xpack.securitySolution.endpoint.details.policyResponse.read_kernel_config": "读取内核配置", - "xpack.securitySolution.endpoint.details.policyResponse.read_logging_config": "读取日志配置", - "xpack.securitySolution.endpoint.details.policyResponse.read_malware_config": "读取恶意软件配置", - "xpack.securitySolution.endpoint.details.policyResponse.streaming": "流式传输", - "xpack.securitySolution.endpoint.details.policyResponse.success": "成功", - "xpack.securitySolution.endpoint.details.policyResponse.warning": "警告", - "xpack.securitySolution.endpoint.details.policyResponse.workflow": "工作流", - "xpack.securitySolution.endpoint.list.loadingPolicies": "正在加载政策配置", - "xpack.securitySolution.endpoint.list.noEndpointsInstructions": "您已创建安全策略。现在您需要按照下面的步骤在代理上启用 Elastic Endpoint Security 功能。", - "xpack.securitySolution.endpoint.list.noEndpointsPrompt": "在您的代理上启用 Elastic Endpoint Security", - "xpack.securitySolution.endpoint.list.noPolicies": "没有策略。", - "xpack.securitySolution.endpoint.list.stepOne": "现有策略在下面列出。之后可以对其进行更改。", - "xpack.securitySolution.endpoint.list.stepOneTitle": "选择要用于保护主机的策略", - "xpack.securitySolution.endpoint.list.stepTwo": "为了让您入门,将会为您提供必要的命令。", - "xpack.securitySolution.endpoint.list.stepTwoTitle": "通过采集管理器注册启用 Endpoint Security 的代理", - "xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration": "使用此代理配置的任何代理都会使用基本策略。可以在 Security 应用中对此策略进行更改,Fleet 会将这些更改部署到代理。", "xpack.securitySolution.endpoint.ingestToastMessage": "采集管理器在其设置期间失败。", "xpack.securitySolution.endpoint.ingestToastTitle": "应用无法初始化", - "xpack.securitySolution.endpoint.policy.details.backToListTitle": "返回到策略列表", + "xpack.securitySolution.endpoint.policy.details.backToListTitle": "返回到终端主机", "xpack.securitySolution.endpoint.policy.details.cancel": "取消", "xpack.securitySolution.endpoint.policy.details.detect": "检测", + "xpack.securitySolution.endpoint.policy.details.detectionRulesLink": "相关检测规则", + "xpack.securitySolution.endpoint.policy.details.detectionRulesMessage": "请查看{detectionRulesLink}。在“检测规则”页面上,预置规则标记有“Elastic”。", "xpack.securitySolution.endpoint.policy.details.eventCollection": "事件收集", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total} 事件收集已启用", "xpack.securitySolution.endpoint.policy.details.linux": "Linux", @@ -16184,10 +15941,10 @@ "xpack.securitySolution.endpoint.policy.details.updateConfirm.confirmButtonTitle": "保存并部署更改", "xpack.securitySolution.endpoint.policy.details.updateConfirm.message": "此操作无法撤消。是否确定要继续?", "xpack.securitySolution.endpoint.policy.details.updateConfirm.title": "保存并部署更改", - "xpack.securitySolution.endpoint.policy.details.updateConfirm.warningMessage": "保存这些更改会将更新应用到分配给此策略的所有活动终端", + "xpack.securitySolution.endpoint.policy.details.updateConfirm.warningMessage": "保存这些更改会将更新应用于分配到此代理配置的所有终端。", "xpack.securitySolution.endpoint.policy.details.updateConfirm.warningTitle": "此操作将更新 {hostCount, plural, one {# 个主机} other {# 个主机}}", "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失败!", - "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "策略 {name} 已更新。", + "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "集成 {name} 已更新。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", "xpack.securitySolution.endpoint.policy.details.windows": "Windows", "xpack.securitySolution.endpoint.policy.details.windowsAndMac": "Windows、Mac", @@ -16214,7 +15971,6 @@ "xpack.securitySolution.endpoint.policyDetailType": "类型", "xpack.securitySolution.endpoint.policyList.actionButtonText": "添加 Endpoint Security", "xpack.securitySolution.endpoint.policyList.actionMenu": "打开", - "xpack.securitySolution.endpoint.policyList.agentPolicyAction": "查看代理配置", "xpack.securitySolution.endpoint.policyList.createdAt": "创建日期", "xpack.securitySolution.endpoint.policyList.createdBy": "创建者", "xpack.securitySolution.endpoint.policyList.createNewButton": "创建新策略", @@ -16240,6 +15996,7 @@ "xpack.securitySolution.endpoint.policyList.revision": "修订 {revNumber}", "xpack.securitySolution.endpoint.policyList.updatedAt": "最后更新时间", "xpack.securitySolution.endpoint.policyList.updatedBy": "最后更新者", + "xpack.securitySolution.endpoint.policyList.versionField": "v{version}", "xpack.securitySolution.endpoint.policyList.versionFieldLabel": "版本", "xpack.securitySolution.endpoint.policyList.viewTitleTotalCount": "{totalItemCount, plural, one {# 个策略} other {# 个策略}}", "xpack.securitySolution.endpoint.resolver.eitherLineageLimitExceeded": "下面可视化和事件列表中的一些进程事件无法显示,因为已达到数据限制。", @@ -16253,7 +16010,6 @@ "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.eventDescriptiveName": "{descriptor} {subject}", "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events": "事件", "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.wait": "等候事件......", - "xpack.securitySolution.resolver.panel.nodeList.title": "所有进程事件", "xpack.securitySolution.endpoint.resolver.panel.relatedCounts.numberOfEventsInCrumb": "{totalCount} 个事件", "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing": "找不到相关事件。", "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait": "等候事件......", @@ -16284,16 +16040,6 @@ "xpack.securitySolution.endpoint.resolver.runningTrigger": "正在运行的触发器", "xpack.securitySolution.endpoint.resolver.terminatedProcess": "已终止进程", "xpack.securitySolution.endpoint.resolver.terminatedTrigger": "已终止触发器", - "xpack.securitySolution.endpoint.list.endpointVersion": "版本", - "xpack.securitySolution.endpoint.list.hostname": "主机名", - "xpack.securitySolution.endpoint.list.hostStatus": "主机状态", - "xpack.securitySolution.endpoint.list.hostStatusValue": "{hostStatus, select, online {联机} error {错误} other {脱机}}", - "xpack.securitySolution.endpoint.list.ip": "IP 地址", - "xpack.securitySolution.endpoint.list.lastActive": "上次活动时间", - "xpack.securitySolution.endpoint.list.os": "操作系统", - "xpack.securitySolution.endpoint.list.policy": "政策", - "xpack.securitySolution.endpoint.list.policyStatus": "策略状态", - "xpack.securitySolution.endpoint.list.totalCount": "{totalItemCount, plural, one {# 个主机} other {# 个主机}}", "xpack.securitySolution.endpointManagement.noPermissionsSubText": "似乎采集管理器已禁用。必须启用采集管理器,才能使用此功能。如果您无权启用采集管理器,请联系您的 Kibana 管理员。", "xpack.securitySolution.endpointManagemnet.noPermissionsText": "您没有所需的 Kibana 权限,无法使用 Elastic Security 管理", "xpack.securitySolution.enpdoint.resolver.panelutils.betaBadgeLabel": "公测版", @@ -16351,33 +16097,48 @@ "xpack.securitySolution.eventsViewer.footer.loadingEventsDataLabel": "正在加载事件", "xpack.securitySolution.eventsViewer.showingLabel": "正在显示", "xpack.securitySolution.eventsViewer.unit": "{totalCount, plural, =1 {事件} other {事件}}", + "xpack.securitySolution.exceptions.addException.addEndpointException": "添加终端例外", "xpack.securitySolution.exceptions.addException.addException": "添加例外", - "xpack.securitySolution.exceptions.addException.bulkCloseLabel": "关闭匹配此例外中的属性的所有告警", + "xpack.securitySolution.exceptions.addException.bulkCloseLabel": "关闭所有与此例外匹配的告警,包括其他规则所生成的告警", "xpack.securitySolution.exceptions.addException.bulkCloseLabel.disabled": "关闭匹配此例外中的属性的所有告警(不支持列表和非 ECS 字段)", "xpack.securitySolution.exceptions.addException.cancel": "取消", - "xpack.securitySolution.exceptions.addException.endpointQuarantineText": "在任何终端上匹配选定属性的任何隔离文件将自动还原到其原始位置", + "xpack.securitySolution.exceptions.addException.endpointQuarantineText": "在所有终端主机上,与该例外匹配的已隔离文件会自动还原到其原始位置。此例外适用于使用终端例外的所有规则。", "xpack.securitySolution.exceptions.addException.error": "添加例外失败", "xpack.securitySolution.exceptions.addException.fetchError": "提取例外列表时出错", "xpack.securitySolution.exceptions.addException.fetchError.title": "错误", "xpack.securitySolution.exceptions.addException.infoLabel": "满足规则的条件时生成告警,但以下情况除外:", "xpack.securitySolution.exceptions.addException.success": "已成功添加例外", "xpack.securitySolution.exceptions.andDescription": "且", + "xpack.securitySolution.exceptions.builder.addNestedDescription": "添加嵌套条件", + "xpack.securitySolution.exceptions.builder.addNonNestedDescription": "添加非嵌套条件", + "xpack.securitySolution.exceptions.builder.exceptionFieldNestedPlaceholderDescription": "搜索嵌套字段", + "xpack.securitySolution.exceptions.builder.exceptionFieldPlaceholderDescription": "搜索", + "xpack.securitySolution.exceptions.builder.exceptionFieldValuePlaceholderDescription": "搜索字段值......", + "xpack.securitySolution.exceptions.builder.exceptionListsPlaceholderDescription": "搜索列表......", + "xpack.securitySolution.exceptions.builder.exceptionOperatorPlaceholderDescription": "运算符", + "xpack.securitySolution.exceptions.builder.fieldDescription": "字段", + "xpack.securitySolution.exceptions.builder.operatorDescription": "运算符", + "xpack.securitySolution.exceptions.builder.valueDescription": "值", "xpack.securitySolution.exceptions.commentEventLabel": "已添加注释", "xpack.securitySolution.exceptions.commentLabel": "注释", "xpack.securitySolution.exceptions.createdByLabel": "创建者", "xpack.securitySolution.exceptions.dateCreatedLabel": "创建日期", + "xpack.securitySolution.exceptions.descriptionLabel": "描述", "xpack.securitySolution.exceptions.detectionListLabel": "检测列表", "xpack.securitySolution.exceptions.doesNotExistOperatorLabel": "不存在", "xpack.securitySolution.exceptions.editButtonLabel": "编辑", - "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "关闭匹配此例外中的属性的所有告警", + "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "关闭所有与此例外匹配的告警,包括其他规则所生成的告警", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "关闭匹配此例外中的属性的所有告警(不支持列表和非 ECS 字段)", "xpack.securitySolution.exceptions.editException.cancel": "取消", + "xpack.securitySolution.exceptions.editException.editEndpointExceptionTitle": "编辑终端例外", "xpack.securitySolution.exceptions.editException.editExceptionSaveButton": "保存", "xpack.securitySolution.exceptions.editException.editExceptionTitle": "编辑例外", - "xpack.securitySolution.exceptions.editException.endpointQuarantineText": "在任何终端上匹配选定属性的任何隔离文件将自动还原到其原始位置", + "xpack.securitySolution.exceptions.editException.endpointQuarantineText": "在所有终端主机上,与该例外匹配的已隔离文件会自动还原到其原始位置。此例外适用于使用终端例外的所有规则。", "xpack.securitySolution.exceptions.editException.error": "更新例外失败", "xpack.securitySolution.exceptions.editException.infoLabel": "满足规则的条件时生成告警,但以下情况除外:", "xpack.securitySolution.exceptions.editException.success": "已成功更新例外", + "xpack.securitySolution.exceptions.editException.versionConflictDescription": "此例外可能自您首次选择编辑后已更新。尝试单击“取消”,重新编辑该例外。", + "xpack.securitySolution.exceptions.editException.versionConflictTitle": "抱歉,有错误", "xpack.securitySolution.exceptions.endpointListLabel": "终端列表", "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "每页项数:{items}", "xpack.securitySolution.exceptions.existsOperatorLabel": "存在", @@ -16400,9 +16161,9 @@ "xpack.securitySolution.exceptions.valueDescription": "值", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "添加新注释......", "xpack.securitySolution.exceptions.viewer.addExceptionLabel": "添加新例外", - "xpack.securitySolution.exceptions.viewer.addToClipboard": "添加到剪贴板", - "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "添加到检测列表", - "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "添加到终端列表", + "xpack.securitySolution.exceptions.viewer.addToClipboard": "注释", + "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "添加规则例外", + "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "添加终端例外", "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "删除例外时出错", "xpack.securitySolution.exceptions.viewer.emptyPromptBody": "可以添加例外以微调规则,以便在满足例外条件时不创建检测告警。例外提升检测精确性,从而可以减少误报数。", "xpack.securitySolution.exceptions.viewer.emptyPromptTitle": "此规则没有例外", @@ -16411,6 +16172,8 @@ "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription": "此规则的所有例外将应用到终端和检测规则。查看{ruleSettings}以了解详情。", "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription.ruleSettingsLink": "规则设置", "xpack.securitySolution.exceptions.viewer.fetchingListError": "提取例外时出错", + "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "获取例外项总数时出错", + "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "找不到搜索结果。", "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "搜索字段(例如:host.name)", "xpack.securitySolution.exitFullScreenButton": "退出全屏", "xpack.securitySolution.featureRegistry.linkSecuritySolutionTitle": "安全", @@ -16459,11 +16222,12 @@ "xpack.securitySolution.header.editableTitle.editButtonAria": "通过单击,可以编辑 {title}", "xpack.securitySolution.header.editableTitle.save": "保存", "xpack.securitySolution.headerGlobal.buttonAddData": "添加数据", + "xpack.securitySolution.headerGlobal.securitySolution": "安全解决方案", "xpack.securitySolution.headerPage.pageSubtitle": "上一事件:{beat}", "xpack.securitySolution.hooks.useAddToTimeline.addedFieldMessage": "已将 {fieldOrValue} 添加到时间线", "xpack.securitySolution.host.details.architectureLabel": "架构", - "xpack.securitySolution.host.details.endpoint.endpointPolicy": "终端策略", - "xpack.securitySolution.host.details.endpoint.policyStatus": "策略状态", + "xpack.securitySolution.host.details.endpoint.endpointPolicy": "集成", + "xpack.securitySolution.host.details.endpoint.policyStatus": "配置状态", "xpack.securitySolution.host.details.endpoint.sensorversion": "感应器版本", "xpack.securitySolution.host.details.firstSeenTitle": "首次看到时间", "xpack.securitySolution.host.details.lastSeenTitle": "最后看到时间", @@ -16480,8 +16244,6 @@ "xpack.securitySolution.host.details.overview.platformTitle": "平台", "xpack.securitySolution.host.details.overview.regionTitle": "地区", "xpack.securitySolution.host.details.versionLabel": "版本", - "xpack.securitySolution.endpoint.list.pageSubTitle": "运行 Elastic Endpoint Security 的主机", - "xpack.securitySolution.endpoint.list.pageTitle": "主机", "xpack.securitySolution.hosts.kqlPlaceholder": "例如 host.name:“foo”", "xpack.securitySolution.hosts.navigation.alertsTitle": "外部告警", "xpack.securitySolution.hosts.navigation.allHostsTitle": "所有主机", @@ -16493,7 +16255,6 @@ "xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingAuthenticationsData": "无法查询身份验证数据", "xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingEventsData": "无法查询事件数据", "xpack.securitySolution.hosts.pageTitle": "主机", - "xpack.securitySolution.endpointsTab": "主机", "xpack.securitySolution.hostsTable.firstLastSeenToolTip": "相对于选定日期范围", "xpack.securitySolution.hostsTable.hostsTitle": "所有主机", "xpack.securitySolution.hostsTable.lastSeenTitle": "最后看到时间", @@ -16538,12 +16299,17 @@ "xpack.securitySolution.lists.cancelValueListsUploadTitle": "取消上传", "xpack.securitySolution.lists.closeValueListsModalTitle": "关闭", "xpack.securitySolution.lists.detectionEngine.rules.uploadValueListsButton": "上传值列表", - "xpack.securitySolution.lists.uploadValueListDescription": "上传编写规则或规则例外时要使用的单值列表。", + "xpack.securitySolution.lists.detectionEngine.rules.uploadValueListsButtonTooltip": "在字段值与列表中找到的值匹配时,使用值列表创建例外", + "xpack.securitySolution.lists.uploadValueListDescription": "上传编写规则例外时要使用的单值列表。", + "xpack.securitySolution.lists.uploadValueListExtensionValidationMessage": "文件必须属于以下类型之一:[{fileTypes}]", "xpack.securitySolution.lists.uploadValueListPrompt": "选择或拖放文件", "xpack.securitySolution.lists.uploadValueListTitle": "上传值列表", + "xpack.securitySolution.lists.valueListsExportError": "导出值列表时出错。", "xpack.securitySolution.lists.valueListsForm.ipRadioLabel": "IP 地址", + "xpack.securitySolution.lists.valueListsForm.ipRangesRadioLabel": "IP 范围", "xpack.securitySolution.lists.valueListsForm.keywordsRadioLabel": "关键字", "xpack.securitySolution.lists.valueListsForm.listTypesRadioLabel": "值列表类型", + "xpack.securitySolution.lists.valueListsForm.textRadioLabel": "文本", "xpack.securitySolution.lists.valueListsTable.actionsColumn": "操作", "xpack.securitySolution.lists.valueListsTable.createdByColumn": "创建者", "xpack.securitySolution.lists.valueListsTable.deleteActionDescription": "删除值列表", @@ -16752,8 +16518,8 @@ "xpack.securitySolution.overview.endpointNotice.dismiss": "关闭消息", "xpack.securitySolution.overview.endpointNotice.introducing": "即将引入: ", "xpack.securitySolution.overview.endpointNotice.message": "使用威胁防御、检测和深度安全数据可见性来保护您的主机。", - "xpack.securitySolution.overview.endpointNotice.title": "Elastic Endpoint Security 公测版", - "xpack.securitySolution.overview.endpointNotice.tryButton": "试用 Elastic Endpoint Security 公测版", + "xpack.securitySolution.overview.endpointNotice.title": "Elastic Endpoint Security(公测版)", + "xpack.securitySolution.overview.endpointNotice.tryButton": "试用 Elastic Endpoint Security(公测版)", "xpack.securitySolution.overview.eventsTitle": "事件计数", "xpack.securitySolution.overview.feedbackText": "如果您对 Elastic SIEM 体验有任何建议,请随时{feedback}。", "xpack.securitySolution.overview.feedbackText.feedbackLinkText": "在线提交反馈", @@ -16799,9 +16565,14 @@ "xpack.securitySolution.overview.viewEventsButtonLabel": "查看事件", "xpack.securitySolution.overview.winlogbeatMWSysmonOperational": "Microsoft-Windows-Sysmon/Operational", "xpack.securitySolution.overview.winlogbeatSecurityTitle": "安全", - "xpack.securitySolution.pages.common.emptyActionEndpoint": "使用 Elastic 代理添加数据(公测版)", + "xpack.securitySolution.pages.common.emptyActionBeats": "使用 Beats 添加数据", + "xpack.securitySolution.pages.common.emptyActionBeatsDescription": "轻量型 Beats 可以发送来自成百上千的机器和系统中的数据", + "xpack.securitySolution.pages.common.emptyActionElasticAgent": "使用 Elastic 代理添加数据", + "xpack.securitySolution.pages.common.emptyActionElasticAgentDescription": "通过 Elastic 代理,可以简单统一的方式将监测添加到主机。", + "xpack.securitySolution.pages.common.emptyActionEndpoint": "添加 Elastic Endpoint Security", + "xpack.securitySolution.pages.common.emptyActionEndpointDescription": "使用威胁防御、检测和深度安全数据可见性功能保护您的主机。", "xpack.securitySolution.pages.common.emptyActionSecondary": "入门指南。", - "xpack.securitySolution.pages.common.emptyTitle": "欢迎使用安全解决方案。让我们教您如何入门。", + "xpack.securitySolution.pages.common.emptyTitle": "欢迎使用 Elastic Security。让我们帮您如何入门。", "xpack.securitySolution.pages.fourohfour.noContentFoundDescription": "未找到任何内容", "xpack.securitySolution.paginatedTable.rowsButtonLabel": "每页行数", "xpack.securitySolution.paginatedTable.showingSubtitle": "正在显示", @@ -16905,7 +16676,7 @@ "xpack.securitySolution.timeline.body.renderers.endgame.withSpecialPrivilegesDescription": "使用特殊权限,", "xpack.securitySolution.timeline.body.sort.sortedAscendingTooltip": "已升序", "xpack.securitySolution.timeline.body.sort.sortedDescendingTooltip": "已降序", - "xpack.securitySolution.timeline.callOut.immutable.message.description": "此时间线不可变,因此不允许在 Security 应用程序中进行保存,但您可以继续使用该时间线搜索并筛选安全事件", + "xpack.securitySolution.timeline.callOut.immutable.message.description": "此预置时间线模板无法修改。要执行更改,请复制此模板,然后修改复制的模板。", "xpack.securitySolution.timeline.callOut.unauthorized.message.description": "可以使用“时间线”调查事件,但您没有所需的权限来保存时间线以供将来使用。如果需要保存时间线,请联系您的 Kibana 管理员。", "xpack.securitySolution.timeline.categoryTooltip": "类别", "xpack.securitySolution.timeline.defaultTimelineDescription": "创建新的时间线时默认提供的时间线。", @@ -16927,6 +16698,7 @@ "xpack.securitySolution.timeline.flyoutTimelineTemplateLabel": "时间线模板", "xpack.securitySolution.timeline.fullScreenButton": "全屏", "xpack.securitySolution.timeline.graphOverlay.backToEventsButton": "< 返回至事件", + "xpack.securitySolution.timeline.properties.attachTimelineToCaseTooltip": "请为您的时间线提供标题,以便将其附加到案例", "xpack.securitySolution.timeline.properties.attachToExistingCaseButtonLabel": "附加到现有案例......", "xpack.securitySolution.timeline.properties.attachToNewCaseButtonLabel": "附加到新案例", "xpack.securitySolution.timeline.properties.descriptionPlaceholder": "描述", @@ -17836,8 +17608,13 @@ "xpack.spaces.management.confirmDeleteModal.deletingSpaceWarningMessage": "删除空间会永久删除空间及其 {allContents}。此操作无法撤消。", "xpack.spaces.management.confirmDeleteModal.redirectAfterDeletingCurrentSpaceWarningMessage": "您即将删除当前空间 {name}。如果继续,系统会将您重定向到选择其他空间的位置。", "xpack.spaces.management.confirmDeleteModal.spaceNamesDoNoMatchErrorMessage": "空间名称不匹配。", + "xpack.spaces.management.copyToSpace.actionDescription": "将此已保存对象复制到一个或多个工作区", + "xpack.spaces.management.copyToSpace.actionTitle": "复制到工作区", "xpack.spaces.management.copyToSpace.copyErrorTitle": "复制已保存对象时出错", + "xpack.spaces.management.copyToSpace.copyResultsLabel": "复制结果", "xpack.spaces.management.copyToSpace.copyStatus.conflictsMessage": "具有匹配 ID ({id}) 的已保存对象在此工作区中已存在。", + "xpack.spaces.management.copyToSpace.copyStatus.conflictsOverwriteMessage": "单击“覆盖”可将此版本替换为复制的版本。", + "xpack.spaces.management.copyToSpace.copyStatus.pendingOverwriteMessage": "已保存对象将被覆盖。单击“跳过”可取消此操作。", "xpack.spaces.management.copyToSpace.copyStatus.successMessage": "已保存对象成功复制。", "xpack.spaces.management.copyToSpace.copyStatus.unresolvableErrorMessage": "复制此已保存对象时出错。", "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "在 {space} 工作区中检测到一个或多个冲突。展开此部分以进行解决。", @@ -17845,17 +17622,24 @@ "xpack.spaces.management.copyToSpace.copyStatusSummary.successMessage": "已成功复制到 {space} 工作区。", "xpack.spaces.management.copyToSpace.copyToSpacesButton": "复制到 {spaceCount} {spaceCount, plural, one {个工作区} other {个工作区}}", "xpack.spaces.management.copyToSpace.disabledCopyToSpacesButton": "复制", + "xpack.spaces.management.copyToSpace.dontIncludeRelatedLabel": "不包括相关已保存对象", + "xpack.spaces.management.copyToSpace.dontOverwriteLabel": "未覆盖已保存对象", "xpack.spaces.management.copyToSpace.finishCopyToSpacesButton": "完成", "xpack.spaces.management.copyToSpace.finishedButtonLabel": "复制已完成。", + "xpack.spaces.management.copyToSpace.finishPendingOverwritesCopyToSpacesButton": "覆盖 {overwriteCount} 个对象", + "xpack.spaces.management.copyToSpace.includeRelatedLabel": "包括相关已保存对象", "xpack.spaces.management.copyToSpace.inProgressButtonLabel": "复制正在进行中。请稍候。", "xpack.spaces.management.copyToSpace.noSpacesBody": "没有可向其中进行复制的合格工作区。", "xpack.spaces.management.copyToSpace.noSpacesTitle": "没有可用的工作区", "xpack.spaces.management.copyToSpace.overwriteLabel": "正在自动覆盖已保存对象", "xpack.spaces.management.copyToSpace.resolveCopyErrorTitle": "解决已保存对象冲突时出错", + "xpack.spaces.management.copyToSpace.resolveCopySuccessTitle": "覆盖成功", + "xpack.spaces.management.copyToSpace.selectSpacesLabel": "选择要向其中进行复制的工作区", "xpack.spaces.management.copyToSpace.spacesLoadErrorTitle": "加载可用工作区时出错", "xpack.spaces.management.copyToSpaceFlyoutFooter.errorCount": "错误", "xpack.spaces.management.copyToSpaceFlyoutFooter.pendingCount": "待处理", "xpack.spaces.management.copyToSpaceFlyoutFooter.successCount": "已复制", + "xpack.spaces.management.copyToSpaceFlyoutHeader": "将已保存对象复制到工作区", "xpack.spaces.management.createSpaceBreadcrumb": "创建", "xpack.spaces.management.customizeSpaceAvatar.colorFormRowLabel": "颜色", "xpack.spaces.management.customizeSpaceAvatar.imageUrl": "定制图像", @@ -18335,6 +18119,7 @@ "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.titleFieldLabel": "简短描述", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.urgencySelectFieldLabel": "紧急度", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel": "用户名", + "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowAction.apiUrlHelpLabel": "配置 ServiceNow 的个人开发人员实例", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "发送到 Slack", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText": "Webhook URL 必填。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel": "消息", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts index f5670f432d4d4..48544945836d9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const SERVICENOW_DESC = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.selectMessageText', { - defaultMessage: 'Push or update data to a new incident in ServiceNow.', + defaultMessage: 'Create an incident in ServiceNow.', } ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx index 78252dccd20d2..9cc64defc1795 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx @@ -15,7 +15,7 @@ import { urlDrilldownActionFactory, } from './test_data'; import { ActionFactory } from '../../dynamic_actions'; -import { licenseMock } from '../../../../licensing/common/licensing.mock'; +import { licensingMock } from '../../../../licensing/public/mocks'; // TODO: afterEach is not available for it globally during setup // https://github.com/elastic/kibana/issues/59469 @@ -68,8 +68,12 @@ test('If not enough license, button is disabled', () => { { ...urlDrilldownActionFactory, minimalLicense: 'gold', + licenseFeatureName: 'Url Drilldown', }, - () => licenseMock.createLicense() + { + getLicense: () => licensingMock.createLicense(), + getFeatureUsageStart: () => licensingMock.createStart().featureUsage, + } ); const screen = render(); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx index 7e4fe1de8be8d..16d0250c5721e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx @@ -21,7 +21,12 @@ import { EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { txtChangeButton, txtTriggerPickerHelpText, txtTriggerPickerLabel } from './i18n'; +import { + txtChangeButton, + txtTriggerPickerHelpText, + txtTriggerPickerLabel, + txtTriggerPickerHelpTooltip, +} from './i18n'; import './action_wizard.scss'; import { ActionFactory, BaseActionFactoryContext } from '../../dynamic_actions'; import { Trigger, TriggerId } from '../../../../../../src/plugins/ui_actions/public'; @@ -93,7 +98,7 @@ export const ActionWizard: React.FC = ({ if ( !currentActionFactory && actionFactories.length === 1 && - actionFactories[0].isCompatibleLicence() + actionFactories[0].isCompatibleLicense() ) { onActionFactoryChange(actionFactories[0]); } @@ -157,14 +162,17 @@ const TriggerPicker: React.FC = ({ const selectedTrigger = selectedTriggers ? selectedTriggers[0] : undefined; return (
    {txtTriggerPickerLabel}{' '} - - {txtTriggerPickerHelpText} - + + + {txtTriggerPickerHelpText} + +
    ), @@ -271,7 +279,7 @@ const SelectedActionFactory: React.FC = ({ /> )} - +
    = ({ * make sure not compatible factories are in the end */ const ensureOrder = (factories: ActionFactory[]) => { - const compatibleLicense = factories.filter((f) => f.isCompatibleLicence()); - const notCompatibleLicense = factories.filter((f) => !f.isCompatibleLicence()); + const compatibleLicense = factories.filter((f) => f.isCompatibleLicense()); + const notCompatibleLicense = factories.filter((f) => !f.isCompatibleLicense()); return [ ...compatibleLicense.sort((f1, f2) => f2.order - f1.order), ...notCompatibleLicense.sort((f1, f2) => f2.order - f1.order), @@ -328,7 +336,7 @@ const ActionFactorySelector: React.FC = ({ = ({ label={actionFactory.getDisplayName(context)} data-test-subj={`${TEST_SUBJ_ACTION_FACTORY_ITEM}-${actionFactory.id}`} onClick={() => onActionFactorySelected(actionFactory)} - disabled={!actionFactory.isCompatibleLicence()} + disabled={!actionFactory.isCompatibleLicense()} > {actionFactory.getIconType(context) && ( diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts index 678457f9794f3..f494ecfb51f32 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts @@ -16,13 +16,20 @@ export const txtChangeButton = i18n.translate( export const txtTriggerPickerLabel = i18n.translate( 'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerLabel', { - defaultMessage: 'Pick a trigger:', + defaultMessage: 'Show option on:', } ); export const txtTriggerPickerHelpText = i18n.translate( - 'xpack.uiActionsEnhanced.components.actionWizard.helpText', + 'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpText', { defaultMessage: "What's this?", } ); + +export const txtTriggerPickerHelpTooltip = i18n.translate( + 'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerHelpTooltip', + { + defaultMessage: 'Determines when the drilldown appears in context menu', + } +); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx index d48cb13b1a470..71286e9a59c06 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx @@ -10,7 +10,7 @@ import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/p import { ActionWizard } from './action_wizard'; import { ActionFactory, ActionFactoryDefinition } from '../../dynamic_actions'; import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public'; -import { licenseMock } from '../../../../licensing/common/licensing.mock'; +import { licensingMock } from '../../../../licensing/public/mocks'; import { APPLY_FILTER_TRIGGER, SELECT_RANGE_TRIGGER, @@ -116,9 +116,10 @@ export const dashboardDrilldownActionFactory: ActionFactoryDefinition< }, }; -export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory, () => - licenseMock.createLicense() -); +export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory, { + getLicense: () => licensingMock.createLicense(), + getFeatureUsageStart: () => licensingMock.createStart().featureUsage, +}); interface UrlDrilldownConfig { url: string; @@ -176,9 +177,10 @@ export const urlDrilldownActionFactory: ActionFactoryDefinition - licenseMock.createLicense() -); +export const urlFactory = new ActionFactory(urlDrilldownActionFactory, { + getLicense: () => licensingMock.createLicense(), + getFeatureUsageStart: () => licensingMock.createStart().featureUsage, +}); export const mockSupportedTriggers: TriggerId[] = [ VALUE_CLICK_TRIGGER, diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index 9fca785ec9072..8154ec45b8ae1 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -30,7 +30,7 @@ import { SerializedAction, SerializedEvent, } from '../../../dynamic_actions'; -import { ExtraActionFactoryContext } from '../types'; +import { ActionFactoryPlaceContext } from '../types'; interface ConnectedFlyoutManageDrilldownsProps< ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext @@ -47,7 +47,7 @@ interface ConnectedFlyoutManageDrilldownsProps< /** * Extra action factory context passed into action factories CollectConfig, getIconType, getDisplayName and etc... */ - extraContext?: ExtraActionFactoryContext; + placeContext?: ActionFactoryPlaceContext; } /** @@ -81,8 +81,8 @@ export function createFlyoutManageDrilldowns({ const isCreateOnly = props.viewMode === 'create'; const factoryContext: BaseActionFactoryContext = useMemo( - () => ({ ...props.extraContext, triggers: props.supportedTriggers }), - [props.extraContext, props.supportedTriggers] + () => ({ ...props.placeContext, triggers: props.supportedTriggers }), + [props.placeContext, props.supportedTriggers] ); const actionFactories = useCompatibleActionFactoriesForCurrentContext( allActionFactories, @@ -137,7 +137,7 @@ export function createFlyoutManageDrilldowns({ function mapToDrilldownToDrilldownListItem(drilldown: SerializedEvent): DrilldownListItem { const actionFactory = allActionFactoriesById[drilldown.action.factoryId]; const drilldownFactoryContext: BaseActionFactoryContext = { - ...props.extraContext, + ...props.placeContext, triggers: drilldown.triggers as TriggerId[], }; return { @@ -148,7 +148,7 @@ export function createFlyoutManageDrilldowns({ icon: actionFactory?.getIconType(drilldownFactoryContext), error: !actionFactory ? invalidDrilldownType(drilldown.action.factoryId) // this shouldn't happen for the end user, but useful during development - : !actionFactory.isCompatibleLicence() + : !actionFactory.isCompatibleLicense() ? insufficientLicenseLevel : undefined, triggers: drilldown.triggers.map((trigger) => getTrigger(trigger as TriggerId)), @@ -204,7 +204,7 @@ export function createFlyoutManageDrilldowns({ setRoute(Routes.Manage); setCurrentEditId(null); }} - extraActionFactoryContext={props.extraContext} + actionFactoryPlaceContext={props.placeContext} initialDrilldownWizardConfig={resolveInitialDrilldownWizardConfig()} supportedTriggers={props.supportedTriggers} getTrigger={getTrigger} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx index a908d53bf6ae7..c8e3f454bd53d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx @@ -18,7 +18,7 @@ import { import { DrilldownHelloBar } from '../drilldown_hello_bar'; import { ActionFactory, BaseActionFactoryContext } from '../../../dynamic_actions'; import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; -import { ExtraActionFactoryContext } from '../types'; +import { ActionFactoryPlaceContext } from '../types'; export interface DrilldownWizardConfig { name: string; @@ -44,7 +44,7 @@ export interface FlyoutDrilldownWizardProps< showWelcomeMessage?: boolean; onWelcomeHideClick?: () => void; - extraActionFactoryContext?: ExtraActionFactoryContext; + actionFactoryPlaceContext?: ActionFactoryPlaceContext; docsLink?: string; @@ -143,7 +143,7 @@ export function FlyoutDrilldownWizard ({ - ...extraActionFactoryContext, + ...actionFactoryPlaceContext, triggers: wizardConfig.selectedTriggers ?? [], }), - [extraActionFactoryContext, wizardConfig.selectedTriggers] + [actionFactoryPlaceContext, wizardConfig.selectedTriggers] ); const isActionValid = ( diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx index bb3eb89d8f199..d7f94a52088b7 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -75,7 +75,7 @@ export const FormDrilldownWizard: React.FC = ({ ); const hasNotCompatibleLicenseFactory = () => - actionFactories?.some((f) => !f.isCompatibleLicence()); + actionFactories?.some((f) => !f.isCompatibleLicense()); const renderGetMoreActionsLink = () => ( diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts index 870b55c24fb58..811680bf380f7 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts @@ -10,6 +10,6 @@ import { BaseActionFactoryContext } from '../../dynamic_actions'; * Interface used as piece of ActionFactoryContext that is passed in from drilldown wizard component to action factories * Omitted values are added inside the wizard and then full {@link BaseActionFactoryContext} passed into action factory methods */ -export type ExtraActionFactoryContext< +export type ActionFactoryPlaceContext< ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext > = Omit; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index ff455c6ae45b6..8faccc088a327 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -37,11 +37,18 @@ export interface DrilldownDefinition< id: string; /** - * Minimal licence level + * Minimal license level * Empty means no restrictions */ minimalLicense?: LicenseType; + /** + * Required when `minimalLicense` is used. + * Is a user-facing string. Has to be unique. Doesn't need i18n. + * The feature's name will be displayed to Cloud end-users when they're billed based on their feature usage. + */ + licenseFeatureName?: string; + /** * Determines the display order of the drilldowns in the flyout picker. * Higher numbers are displayed first. diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md new file mode 100644 index 0000000000000..acad968fa46c2 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md @@ -0,0 +1 @@ +This directory contains reusable building blocks for creating custom URL drilldowns diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts new file mode 100644 index 0000000000000..70399617136bd --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { UrlDrilldownCollectConfig } from './url_drilldown_collect_config/url_drilldown_collect_config'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts new file mode 100644 index 0000000000000..78f7218dce22e --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts @@ -0,0 +1,80 @@ +/* + * 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 const txtUrlTemplatePlaceholder = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText', + { + defaultMessage: 'Example: {exampleUrl}', + values: { + exampleUrl: 'https://www.my-url.com/?{{event.key}}={{event.value}}', + }, + } +); + +export const txtUrlPreviewHelpText = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText', + { + defaultMessage: 'Please note that \\{\\{event.*\\}\\} variables replaced by dummy values.', + } +); + +export const txtAddVariableButtonTitle = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle', + { + defaultMessage: 'Add variable', + } +); + +export const txtUrlTemplateLabel = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel', + { + defaultMessage: 'Enter URL template:', + } +); + +export const txtUrlTemplateSyntaxHelpLinkText = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText', + { + defaultMessage: 'Syntax help', + } +); + +export const txtUrlTemplateVariablesHelpLinkText = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText', + { + defaultMessage: 'Help', + } +); + +export const txtUrlTemplateVariablesFilterPlaceholderText = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText', + { + defaultMessage: 'Filter variables', + } +); + +export const txtUrlTemplatePreviewLabel = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel', + { + defaultMessage: 'URL preview:', + } +); + +export const txtUrlTemplatePreviewLinkText = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText', + { + defaultMessage: 'Preview', + } +); + +export const txtUrlTemplateOpenInNewTab = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel', + { + defaultMessage: 'Open in new tab', + } +); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss new file mode 100644 index 0000000000000..475c3f2a915ea --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss @@ -0,0 +1,5 @@ +.uaeUrlDrilldownCollectConfig__urlTemplateFormRow { + .euiFormRow__label { + align-self: flex-end; + } +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx new file mode 100644 index 0000000000000..e6c9797623e9f --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx @@ -0,0 +1,47 @@ +/* + * 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 React from 'react'; +import { UrlDrilldownConfig, UrlDrilldownScope } from '../../../types'; +import { UrlDrilldownCollectConfig } from '../url_drilldown_collect_config'; + +export const Demo = () => { + const [config, onConfig] = React.useState({ + openInNewTab: false, + url: { template: '' }, + }); + + const fakeScope: UrlDrilldownScope = { + kibanaUrl: 'http://localhost:5601/', + context: { + filters: [ + { + query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + { + query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + { + query: { match: { _type: { query: 'nginx', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + ], + }, + event: { + key: 'fakeKey', + value: 'fakeValue', + }, + }; + + return ( + <> + + {JSON.stringify(config)} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx similarity index 55% rename from x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js rename to x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx index 7fe7134f5f5db..244ea9bd2a97e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/reducers/index.js +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { combineReducers } from 'redux'; -import { policies } from './policies'; +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { Demo } from './test_samples/demo'; -export const indexLifecycleManagement = combineReducers({ - policies, -}); +storiesOf('UrlDrilldownCollectConfig', module).add('default', () => ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx new file mode 100644 index 0000000000000..f55818379ef3f --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx @@ -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 { Demo } from './test_samples/demo'; +import { cleanup, fireEvent, render } from '@testing-library/react/pure'; +import React from 'react'; + +afterEach(cleanup); + +test('configure valid URL template', () => { + const screen = render(); + + const urlTemplate = 'https://elastic.co/?{{event.key}}={{event.value}}'; + fireEvent.change(screen.getByLabelText(/Enter URL template/i), { + target: { value: urlTemplate }, + }); + + const preview = screen.getByLabelText(/URL preview/i) as HTMLTextAreaElement; + expect(preview.value).toMatchInlineSnapshot(`"https://elastic.co/?fakeKey=fakeValue"`); + expect(preview.disabled).toEqual(true); + const previewLink = screen.getByText('Preview') as HTMLAnchorElement; + expect(previewLink.href).toMatchInlineSnapshot(`"https://elastic.co/?fakeKey=fakeValue"`); + expect(previewLink.target).toMatchInlineSnapshot(`"_blank"`); +}); + +test('configure invalid URL template', () => { + const screen = render(); + + const urlTemplate = 'https://elastic.co/?{{event.wrongKey}}={{event.wrongValue}}'; + fireEvent.change(screen.getByLabelText(/Enter URL template/i), { + target: { value: urlTemplate }, + }); + + const previewTextArea = screen.getByLabelText(/URL preview/i) as HTMLTextAreaElement; + expect(previewTextArea.disabled).toEqual(true); + expect(previewTextArea.value).toEqual(urlTemplate); + expect(screen.getByText(/invalid format/i)).toBeInTheDocument(); // check that error is shown + + const previewLink = screen.getByText('Preview') as HTMLAnchorElement; + expect(previewLink.href).toEqual(urlTemplate); + expect(previewLink.target).toMatchInlineSnapshot(`"_blank"`); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx new file mode 100644 index 0000000000000..dabf09e4b6e9f --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx @@ -0,0 +1,226 @@ +/* + * 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 React, { useRef, useState } from 'react'; +import { + EuiCheckbox, + EuiFormRow, + EuiIcon, + EuiLink, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, + EuiSelectable, + EuiText, + EuiTextArea, + EuiSelectableOption, +} from '@elastic/eui'; +import { UrlDrilldownConfig, UrlDrilldownScope } from '../../types'; +import { compile } from '../../url_template'; +import { validateUrlTemplate } from '../../url_validation'; +import { buildScopeSuggestions } from '../../url_drilldown_scope'; +import './index.scss'; +import { + txtAddVariableButtonTitle, + txtUrlPreviewHelpText, + txtUrlTemplateSyntaxHelpLinkText, + txtUrlTemplateVariablesHelpLinkText, + txtUrlTemplateVariablesFilterPlaceholderText, + txtUrlTemplateLabel, + txtUrlTemplateOpenInNewTab, + txtUrlTemplatePlaceholder, + txtUrlTemplatePreviewLabel, + txtUrlTemplatePreviewLinkText, +} from './i18n'; + +export interface UrlDrilldownCollectConfig { + config: UrlDrilldownConfig; + onConfig: (newConfig: UrlDrilldownConfig) => void; + scope: UrlDrilldownScope; + syntaxHelpDocsLink?: string; +} + +export const UrlDrilldownCollectConfig: React.FC = ({ + config, + onConfig, + scope, + syntaxHelpDocsLink, +}) => { + const textAreaRef = useRef(null); + const urlTemplate = config.url.template ?? ''; + const compiledUrl = React.useMemo(() => { + try { + return compile(urlTemplate, scope); + } catch { + return urlTemplate; + } + }, [urlTemplate, scope]); + const scopeVariables = React.useMemo(() => buildScopeSuggestions(scope), [scope]); + + function updateUrlTemplate(newUrlTemplate: string) { + if (config.url.template !== newUrlTemplate) { + onConfig({ + ...config, + url: { + ...config.url, + template: newUrlTemplate, + }, + }); + } + } + const { error, isValid } = React.useMemo( + () => validateUrlTemplate({ template: urlTemplate }, scope), + [urlTemplate, scope] + ); + const isEmpty = !urlTemplate; + const isInvalid = !isValid && !isEmpty; + return ( + <> + + {txtUrlTemplateSyntaxHelpLinkText} + + ) + } + labelAppend={ + { + if (textAreaRef.current) { + updateUrlTemplate( + urlTemplate.substr(0, textAreaRef.current!.selectionStart) + + `{{${variable}}}` + + urlTemplate.substr(textAreaRef.current!.selectionEnd) + ); + } else { + updateUrlTemplate(urlTemplate + `{{${variable}}}`); + } + }} + /> + } + > + updateUrlTemplate(event.target.value)} + rows={3} + inputRef={textAreaRef} + /> + + + + {txtUrlTemplatePreviewLinkText} + + + } + helpText={txtUrlPreviewHelpText} + > + + + + onConfig({ ...config, openInNewTab: !config.openInNewTab })} + /> + + + ); +}; + +function AddVariableButton({ + variables, + onSelect, + variablesHelpLink, +}: { + variables: string[]; + onSelect: (variable: string) => void; + variablesHelpLink?: string; +}) { + const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); + const closePopover = () => setIsVariablesPopoverOpen(false); + + const options: EuiSelectableOption[] = variables.map((variable: string) => ({ + key: variable, + label: variable, + })); + + return ( + + setIsVariablesPopoverOpen(true)}> + {txtAddVariableButtonTitle} + + + } + isOpen={isVariablesPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + withTitle + > + { + const selected = newOptions.find((o) => o.checked === 'on'); + if (!selected) return; + onSelect(selected.key!); + closePopover(); + }} + listProps={{ + showIcons: false, + }} + > + {(list, search) => ( +
    + {search} + {list} + {variablesHelpLink && ( + + + {txtUrlTemplateVariablesHelpLinkText} + + + )} +
    + )} +
    +
    + ); +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts new file mode 100644 index 0000000000000..7b7a850050c41 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { UrlDrilldownConfig, UrlDrilldownGlobalScope, UrlDrilldownScope } from './types'; +export { UrlDrilldownCollectConfig } from './components'; +export { + validateUrlTemplate as urlDrilldownValidateUrlTemplate, + validateUrl as urlDrilldownValidateUrl, +} from './url_validation'; +export { compile as urlDrilldownCompileUrl } from './url_template'; +export { globalScopeProvider as urlDrilldownGlobalScopeProvider } from './url_drilldown_global_scope'; +export { + buildScope as urlDrilldownBuildScope, + buildScopeSuggestions as urlDrilldownBuildScopeSuggestions, +} from './url_drilldown_scope'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts new file mode 100644 index 0000000000000..31c7481c9d63e --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts @@ -0,0 +1,37 @@ +/* + * 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 interface UrlDrilldownConfig { + url: { format?: 'handlebars_v1'; template: string }; + openInNewTab: boolean; +} + +/** + * URL drilldown has 3 sources for variables: global, context and event variables + */ +export interface UrlDrilldownScope< + ContextScope extends object = object, + EventScope extends object = object +> extends UrlDrilldownGlobalScope { + /** + * Dynamic variables that are differ depending on where drilldown is created and used, + * For example: variables extracted from embeddable panel + */ + context?: ContextScope; + + /** + * Variables extracted from trigger context + */ + event?: EventScope; +} + +/** + * Global static variables like, for example, `kibanaUrl` + * Such variables won’t change depending on a place where url drilldown is used. + */ +export interface UrlDrilldownGlobalScope { + kibanaUrl: string; +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts new file mode 100644 index 0000000000000..afc7fa590a2f0 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.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. + */ + +import { CoreSetup } from 'kibana/public'; +import { UrlDrilldownGlobalScope } from './types'; + +interface UrlDrilldownGlobalScopeDeps { + core: CoreSetup; +} + +export function globalScopeProvider({ + core, +}: UrlDrilldownGlobalScopeDeps): () => UrlDrilldownGlobalScope { + return () => ({ + kibanaUrl: window.location.origin + core.http.basePath.get(), + }); +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts new file mode 100644 index 0000000000000..f95fc5e70ae00 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.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 { buildScope, buildScopeSuggestions } from './url_drilldown_scope'; + +test('buildScopeSuggestions', () => { + expect( + buildScopeSuggestions( + buildScope({ + globalScope: { + kibanaUrl: 'http://localhost:5061/', + }, + eventScope: { + key: '__testKey__', + value: '__testValue__', + }, + contextScope: { + filters: [ + { + query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + { + query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + { + query: { match: { _type: { query: 'nginx', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + ], + query: { + query: '', + language: 'kquery', + }, + }, + }) + ) + ).toMatchInlineSnapshot(` + Array [ + "event.key", + "event.value", + "context.filters", + "context.query.language", + "context.query.query", + "kibanaUrl", + ] + `); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts new file mode 100644 index 0000000000000..d499812a9d5ae --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts @@ -0,0 +1,39 @@ +/* + * 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 partition from 'lodash/partition'; +import { UrlDrilldownGlobalScope, UrlDrilldownScope } from './types'; +import { getFlattenedObject } from '../../../../../../src/core/public'; + +export function buildScope< + ContextScope extends object = object, + EventScope extends object = object +>({ + globalScope, + contextScope, + eventScope, +}: { + globalScope: UrlDrilldownGlobalScope; + contextScope?: ContextScope; + eventScope?: EventScope; +}): UrlDrilldownScope { + return { + ...globalScope, + context: contextScope, + event: eventScope, + }; +} + +/** + * Builds list of variables for suggestion from scope + * keys sorted alphabetically, except {{event.$}} variables are pulled to the top + * @param scope + */ +export function buildScopeSuggestions(scope: UrlDrilldownGlobalScope): string[] { + const allKeys = Object.keys(getFlattenedObject(scope)).sort(); + const [eventKeys, otherKeys] = partition(allKeys, (key) => key.startsWith('event')); + return [...eventKeys, ...otherKeys]; +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts new file mode 100644 index 0000000000000..64b8cc49292b3 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { compile } from './url_template'; +import moment from 'moment-timezone'; + +test('should compile url without variables', () => { + const url = 'https://elastic.co'; + expect(compile(url, {})).toBe(url); +}); + +test('should fail on unknown syntax', () => { + const url = 'https://elastic.co/{{}'; + expect(() => compile(url, {})).toThrowError(); +}); + +test('should fail on not existing variable', () => { + const url = 'https://elastic.co/{{fake}}'; + expect(() => compile(url, {})).toThrowError(); +}); + +test('should fail on not existing nested variable', () => { + const url = 'https://elastic.co/{{fake.fake}}'; + expect(() => compile(url, { fake: {} })).toThrowError(); +}); + +test('should replace existing variable', () => { + const url = 'https://elastic.co/{{foo}}'; + expect(compile(url, { foo: 'bar' })).toMatchInlineSnapshot(`"https://elastic.co/bar"`); +}); + +test('should fail on unknown helper', () => { + const url = 'https://elastic.co/{{fake foo}}'; + expect(() => compile(url, { foo: 'bar' })).toThrowError(); +}); + +describe('json helper', () => { + test('should replace with json', () => { + const url = 'https://elastic.co/{{json foo bar}}'; + expect(compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toMatchInlineSnapshot( + `"https://elastic.co/%5B%7B%22foo%22:%22bar%22%7D,%7B%22bar%22:%22foo%22%7D%5D"` + ); + }); + test('should replace with json and skip encoding', () => { + const url = 'https://elastic.co/{{{json foo bar}}}'; + expect(compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toMatchInlineSnapshot( + `"https://elastic.co/%5B%7B%22foo%22:%22bar%22%7D,%7B%22bar%22:%22foo%22%7D%5D"` + ); + }); + test('should throw on unknown key', () => { + const url = 'https://elastic.co/{{{json fake}}}'; + expect(() => compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toThrowError(); + }); +}); + +describe('rison helper', () => { + test('should replace with rison', () => { + const url = 'https://elastic.co/{{rison foo bar}}'; + expect(compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toMatchInlineSnapshot( + `"https://elastic.co/!((foo:bar),(bar:foo))"` + ); + }); + test('should replace with rison and skip encoding', () => { + const url = 'https://elastic.co/{{{rison foo bar}}}'; + expect(compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toMatchInlineSnapshot( + `"https://elastic.co/!((foo:bar),(bar:foo))"` + ); + }); + test('should throw on unknown key', () => { + const url = 'https://elastic.co/{{{rison fake}}}'; + expect(() => compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toThrowError(); + }); +}); + +describe('date helper', () => { + let spy: jest.SpyInstance; + const date = new Date('2020-08-18T14:45:00.000Z'); + beforeAll(() => { + spy = jest.spyOn(global.Date, 'now').mockImplementation(() => date.valueOf()); + moment.tz.setDefault('UTC'); + }); + afterAll(() => { + spy.mockRestore(); + moment.tz.setDefault('Browser'); + }); + + test('uses datemath', () => { + const url = 'https://elastic.co/{{date time}}'; + expect(compile(url, { time: 'now' })).toMatchInlineSnapshot( + `"https://elastic.co/2020-08-18T14:45:00.000Z"` + ); + }); + + test('can use format', () => { + const url = 'https://elastic.co/{{date time "dddd, MMMM Do YYYY, h:mm:ss a"}}'; + expect(compile(url, { time: 'now' })).toMatchInlineSnapshot( + `"https://elastic.co/Tuesday,%20August%2018th%202020,%202:45:00%20pm"` + ); + }); + + test('throws if missing variable', () => { + const url = 'https://elastic.co/{{date time}}'; + expect(() => compile(url, {})).toThrowError(); + }); + + test("doesn't throw if non valid date", () => { + const url = 'https://elastic.co/{{date time}}'; + expect(compile(url, { time: 'fake' })).toMatchInlineSnapshot(`"https://elastic.co/fake"`); + }); + + test("doesn't throw on boolean or number", () => { + const url = 'https://elastic.co/{{date time}}'; + expect(compile(url, { time: false })).toMatchInlineSnapshot(`"https://elastic.co/false"`); + expect(compile(url, { time: 24 })).toMatchInlineSnapshot( + `"https://elastic.co/1970-01-01T00:00:00.024Z"` + ); + }); + + test('works with ISO string', () => { + const url = 'https://elastic.co/{{date time}}'; + expect(compile(url, { time: date.toISOString() })).toMatchInlineSnapshot( + `"https://elastic.co/2020-08-18T14:45:00.000Z"` + ); + }); + + test('works with ts', () => { + const url = 'https://elastic.co/{{date time}}'; + expect(compile(url, { time: date.valueOf() })).toMatchInlineSnapshot( + `"https://elastic.co/2020-08-18T14:45:00.000Z"` + ); + }); + test('works with ts string', () => { + const url = 'https://elastic.co/{{date time}}'; + expect(compile(url, { time: String(date.valueOf()) })).toMatchInlineSnapshot( + `"https://elastic.co/2020-08-18T14:45:00.000Z"` + ); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts new file mode 100644 index 0000000000000..2c3537636b9da --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts @@ -0,0 +1,75 @@ +/* + * 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 { create as createHandlebars, HelperDelegate, HelperOptions } from 'handlebars'; +import { encode, RisonValue } from 'rison-node'; +import dateMath from '@elastic/datemath'; +import moment, { Moment } from 'moment'; + +const handlebars = createHandlebars(); + +function createSerializationHelper( + fnName: string, + serializeFn: (value: unknown) => string +): HelperDelegate { + return (...args) => { + const { hash } = args.slice(-1)[0] as HelperOptions; + const hasHash = Object.keys(hash).length > 0; + const hasValues = args.length > 1; + if (hasHash && hasValues) { + throw new Error(`[${fnName}]: both value list and hash are not supported`); + } + if (hasHash) { + if (Object.values(hash).some((v) => typeof v === 'undefined')) + throw new Error(`[${fnName}]: unknown variable`); + return serializeFn(hash); + } else { + const values = args.slice(0, -1) as unknown[]; + if (values.some((value) => typeof value === 'undefined')) + throw new Error(`[${fnName}]: unknown variable`); + if (values.length === 0) throw new Error(`[${fnName}]: unknown variable`); + if (values.length === 1) return serializeFn(values[0]); + return serializeFn(values); + } + }; +} + +handlebars.registerHelper('json', createSerializationHelper('json', JSON.stringify)); +handlebars.registerHelper( + 'rison', + createSerializationHelper('rison', (v) => encode(v as RisonValue)) +); + +handlebars.registerHelper('date', (...args) => { + const values = args.slice(0, -1) as [string | Date, string | undefined]; + // eslint-disable-next-line prefer-const + let [date, format] = values; + if (typeof date === 'undefined') throw new Error(`[date]: unknown variable`); + let momentDate: Moment | undefined; + if (typeof date === 'string') { + momentDate = dateMath.parse(date); + if (!momentDate || !momentDate.isValid()) { + const ts = Number(date); + if (!Number.isNaN(ts)) { + momentDate = moment(ts); + } + } + } else { + momentDate = moment(date); + } + + if (!momentDate || !momentDate.isValid()) { + // do not throw error here, because it could be that in preview `__testValue__` is not parsable, + // but in runtime it is + return date; + } + return format ? momentDate.format(format) : momentDate.toISOString(); +}); + +export function compile(url: string, context: object): string { + const template = handlebars.compile(url, { strict: true, noEscape: true }); + return encodeURI(template(context)); +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts new file mode 100644 index 0000000000000..cb6f4a28402d1 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { validateUrl, validateUrlTemplate } from './url_validation'; + +describe('validateUrl', () => { + describe('unsafe urls', () => { + const unsafeUrls = [ + // eslint-disable-next-line no-script-url + 'javascript:evil()', + // eslint-disable-next-line no-script-url + 'JavaScript:abc', + 'evilNewProtocol:abc', + ' \n Java\n Script:abc', + 'javascript:', + 'javascript:', + 'j avascript:', + 'javascript:', + 'javascript:', + 'jav ascript:alert();', + // 'jav\u0000ascript:alert();', CI fails on this one + 'data:;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + 'data:,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + 'data:iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + 'data:text/javascript;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + 'data:application/x-msdownload;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + ]; + + for (const url of unsafeUrls) { + test(`unsafe ${url}`, () => { + expect(validateUrl(url).isValid).toBe(false); + }); + } + }); + + describe('invalid urls', () => { + const invalidUrls = ['elastic.co', 'www.elastic.co', 'test', '', ' ', 'https://']; + for (const url of invalidUrls) { + test(`invalid ${url}`, () => { + expect(validateUrl(url).isValid).toBe(false); + }); + } + }); + + describe('valid urls', () => { + const validUrls = [ + 'https://elastic.co', + 'https://www.elastic.co', + 'http://elastic', + 'mailto:someone', + ]; + for (const url of validUrls) { + test(`valid ${url}`, () => { + expect(validateUrl(url).isValid).toBe(true); + }); + } + }); +}); + +describe('validateUrlTemplate', () => { + test('domain in variable is allowed', () => { + expect( + validateUrlTemplate( + { template: '{{kibanaUrl}}/test' }, + { kibanaUrl: 'http://localhost:5601/app' } + ).isValid + ).toBe(true); + }); + + test('unsafe domain in variable is not allowed', () => { + expect( + // eslint-disable-next-line no-script-url + validateUrlTemplate({ template: '{{kibanaUrl}}/test' }, { kibanaUrl: 'javascript:evil()' }) + .isValid + ).toBe(false); + }); + + test('if missing variable then invalid', () => { + expect( + validateUrlTemplate({ template: '{{url}}/test' }, { kibanaUrl: 'http://localhost:5601/app' }) + .isValid + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts new file mode 100644 index 0000000000000..b32f5d84c6775 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts @@ -0,0 +1,71 @@ +/* + * 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'; +import { UrlDrilldownConfig, UrlDrilldownScope } from './types'; +import { compile } from './url_template'; + +const generalFormatError = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage', + { + defaultMessage: 'Invalid format. Example: {exampleUrl}', + values: { + exampleUrl: 'https://www.my-url.com/?{{event.key}}={{event.value}}', + }, + } +); + +const formatError = (message: string) => + i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage', + { + defaultMessage: 'Invalid format: {message}', + values: { + message, + }, + } + ); + +const SAFE_URL_PATTERN = /^(?:(?:https?|mailto):|[^&:/?#]*(?:[/?#]|$))/gi; +export function validateUrl(url: string): { isValid: boolean; error?: string } { + if (!url) + return { + isValid: false, + error: generalFormatError, + }; + + try { + new URL(url); + if (!url.match(SAFE_URL_PATTERN)) throw new Error(); + return { isValid: true }; + } catch (e) { + return { + isValid: false, + error: generalFormatError, + }; + } +} + +export function validateUrlTemplate( + urlTemplate: UrlDrilldownConfig['url'], + scope: UrlDrilldownScope +): { isValid: boolean; error?: string } { + if (!urlTemplate.template) + return { + isValid: false, + error: generalFormatError, + }; + + try { + const compiledUrl = compile(urlTemplate.template, scope); + return validateUrl(compiledUrl); + } catch (e) { + return { + isValid: false, + error: formatError(e.message), + }; + } +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts index a07fed8486438..032a4a63fe2e9 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts @@ -7,6 +7,7 @@ import { ActionFactory } from './action_factory'; import { ActionFactoryDefinition } from './action_factory_definition'; import { licensingMock } from '../../../licensing/public/mocks'; +import { PublicLicense } from '../../../licensing/public'; const def: ActionFactoryDefinition = { id: 'ACTION_FACTORY_1', @@ -22,34 +23,94 @@ const def: ActionFactoryDefinition = { supportedTriggers: () => [], }; +const featureUsage = licensingMock.createStart().featureUsage; + +const createActionFactory = ( + defOverride: Partial = {}, + license?: Partial +) => { + return new ActionFactory( + { ...def, ...defOverride }, + { + getLicense: () => licensingMock.createLicense({ license }), + getFeatureUsageStart: () => featureUsage, + } + ); +}; + describe('License & ActionFactory', () => { test('no license requirements', async () => { - const factory = new ActionFactory(def, () => licensingMock.createLicense()); + const factory = createActionFactory(); expect(await factory.isCompatible({ triggers: [] })).toBe(true); - expect(factory.isCompatibleLicence()).toBe(true); + expect(factory.isCompatibleLicense()).toBe(true); }); test('not enough license level', async () => { - const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () => - licensingMock.createLicense() - ); + const factory = createActionFactory({ minimalLicense: 'gold', licenseFeatureName: 'Feature' }); expect(await factory.isCompatible({ triggers: [] })).toBe(true); - expect(factory.isCompatibleLicence()).toBe(false); + expect(factory.isCompatibleLicense()).toBe(false); }); - test('licence has expired', async () => { - const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () => - licensingMock.createLicense({ license: { type: 'gold', status: 'expired' } }) + test('license has expired', async () => { + const factory = createActionFactory( + { minimalLicense: 'gold', licenseFeatureName: 'Feature' }, + { type: 'gold', status: 'expired' } ); expect(await factory.isCompatible({ triggers: [] })).toBe(true); - expect(factory.isCompatibleLicence()).toBe(false); + expect(factory.isCompatibleLicense()).toBe(false); }); test('enough license level', async () => { - const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () => - licensingMock.createLicense({ license: { type: 'gold' } }) + const factory = createActionFactory( + { minimalLicense: 'gold', licenseFeatureName: 'Feature' }, + { type: 'gold' } ); + expect(await factory.isCompatible({ triggers: [] })).toBe(true); - expect(factory.isCompatibleLicence()).toBe(true); + expect(factory.isCompatibleLicense()).toBe(true); + }); + + describe('licenseFeatureName', () => { + test('licenseFeatureName is required, if minimalLicense is provided', () => { + expect(() => { + createActionFactory(); + }).not.toThrow(); + + expect(() => { + createActionFactory({ minimalLicense: 'gold', licenseFeatureName: 'feature' }); + }).not.toThrow(); + + expect(() => { + createActionFactory({ minimalLicense: 'gold' }); + }).toThrow(); + }); + + test('"licenseFeatureName"', () => { + expect( + createActionFactory({ minimalLicense: 'gold', licenseFeatureName: 'feature' }) + .licenseFeatureName + ).toBe('feature'); + expect(createActionFactory().licenseFeatureName).toBeUndefined(); + }); + }); + + describe('notifyFeatureUsage', () => { + const spy = jest.spyOn(featureUsage, 'notifyUsage'); + beforeEach(() => { + spy.mockClear(); + }); + test('is not called if no license requirements', async () => { + const action = createActionFactory().create({ name: 'fake', config: {} }); + await action.execute({}); + expect(spy).not.toBeCalled(); + }); + test('is called if has license requirements', async () => { + const action = createActionFactory({ + minimalLicense: 'gold', + licenseFeatureName: 'feature', + }).create({ name: 'fake', config: {} }); + await action.execute({}); + expect(spy).toBeCalledWith('feature'); + }); }); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts index 35e06ab036fc9..35a82adf9896d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts @@ -13,9 +13,14 @@ import { import { ActionFactoryDefinition } from './action_factory_definition'; import { Configurable } from '../../../../../src/plugins/kibana_utils/public'; import { BaseActionFactoryContext, SerializedAction } from './types'; -import { ILicense } from '../../../licensing/public'; +import { ILicense, LicensingPluginStart } from '../../../licensing/public'; import { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public'; +export interface ActionFactoryDeps { + readonly getLicense: () => ILicense; + readonly getFeatureUsageStart: () => LicensingPluginStart['featureUsage']; +} + export class ActionFactory< Config extends object = object, SupportedTriggers extends TriggerId = TriggerId, @@ -31,11 +36,18 @@ export class ActionFactory< FactoryContext, ActionContext >, - protected readonly getLicence: () => ILicense - ) {} + protected readonly deps: ActionFactoryDeps + ) { + if (def.minimalLicense && !def.licenseFeatureName) { + throw new Error( + `ActionFactory [actionFactory.id = ${def.id}] "licenseFeatureName" is required, if "minimalLicense" is provided` + ); + } + } public readonly id = this.def.id; public readonly minimalLicense = this.def.minimalLicense; + public readonly licenseFeatureName = this.def.licenseFeatureName; public readonly order = this.def.order || 0; public readonly MenuItem? = this.def.MenuItem; public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined; @@ -65,13 +77,13 @@ export class ActionFactory< } /** - * Does this action factory licence requirements + * Does this action factory license requirements * compatible with current license? */ - public isCompatibleLicence() { + public isCompatibleLicense() { if (!this.minimalLicense) return true; - const licence = this.getLicence(); - return licence.isAvailable && licence.isActive && licence.hasAtLeast(this.minimalLicense); + const license = this.deps.getLicense(); + return license.isAvailable && license.isActive && license.hasAtLeast(this.minimalLicense); } public create( @@ -81,14 +93,31 @@ export class ActionFactory< return { ...action, isCompatible: async (context: ActionContext): Promise => { - if (!this.isCompatibleLicence()) return false; + if (!this.isCompatibleLicense()) return false; if (!action.isCompatible) return true; return action.isCompatible(context); }, + execute: async (context: ActionContext): Promise => { + this.notifyFeatureUsage(); + return action.execute(context); + }, }; } public supportedTriggers(): SupportedTriggers[] { return this.def.supportedTriggers(); } + + private notifyFeatureUsage(): void { + if (!this.minimalLicense || !this.licenseFeatureName) return; + this.deps + .getFeatureUsageStart() + .notifyUsage(this.licenseFeatureName) + .catch(() => { + // eslint-disable-next-line no-console + console.warn( + `ActionFactory [actionFactory.id = ${this.def.id}] fail notify feature usage.` + ); + }); + } } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts index d79614e47ccd4..91b8c8ec1e5ef 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts @@ -34,11 +34,18 @@ export interface ActionFactoryDefinition< id: string; /** - * Minimal licence level - * Empty means no licence restrictions + * Minimal license level + * Empty means no license restrictions */ readonly minimalLicense?: LicenseType; + /** + * Required when `minimalLicense` is used. + * Is a user-facing string. Has to be unique. Doesn't need i18n. + * The feature's name will be displayed to Cloud end-users when they're billed based on their feature usage. + */ + licenseFeatureName?: string; + /** * This method should return a definition of a new action, normally used to * register it in `ui_actions` registry. diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index 0b0cd39e35e25..39d9dfeca2fd6 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -87,7 +87,9 @@ const setup = ( actions, }); const uiActionsEnhancements = new UiActionsServiceEnhancements({ - getLicenseInfo, + getLicense: getLicenseInfo, + featureUsageSetup: licensingMock.createSetup().featureUsage, + getFeatureUsageStart: () => licensingMock.createStart().featureUsage, }); const manager = new DynamicActionManager({ isCompatible, @@ -671,11 +673,13 @@ describe('DynamicActionManager', () => { const basicActionFactory: ActionFactoryDefinition = { ...actionFactoryDefinition1, minimalLicense: 'basic', + licenseFeatureName: 'Feature 1', }; const goldActionFactory: ActionFactoryDefinition = { ...actionFactoryDefinition2, minimalLicense: 'gold', + licenseFeatureName: 'Feature 2', }; uiActions.registerActionFactory(basicActionFactory); diff --git a/x-pack/plugins/ui_actions_enhanced/public/index.ts b/x-pack/plugins/ui_actions_enhanced/public/index.ts index a255bc28f5c68..4a899b24852a9 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/index.ts @@ -32,3 +32,4 @@ export { } from './dynamic_actions'; export { DrilldownDefinition as UiActionsEnhancedDrilldownDefinition } from './drilldowns'; +export * from './drilldowns/url_drilldown'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts index ff07d6e74a9c0..17a6fc1b955df 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts @@ -11,6 +11,7 @@ import { embeddablePluginMock } from '../../../../src/plugins/embeddable/public/ import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '.'; import { plugin as pluginInitializer } from '.'; import { licensingMock } from '../../licensing/public/mocks'; +import { StartDependencies } from './plugin'; export type Setup = jest.Mocked; export type Start = jest.Mocked; @@ -35,7 +36,7 @@ const createStartContract = (): Start => { }; const createPlugin = ( - coreSetup: CoreSetup = coreMock.createSetup(), + coreSetup: CoreSetup = coreMock.createSetup(), coreStart: CoreStart = coreMock.createStart() ) => { const pluginInitializerContext = coreMock.createPluginInitializerContext(); @@ -47,6 +48,7 @@ const createPlugin = ( const setup = plugin.setup(coreSetup, { uiActions: uiActions.setup, embeddable: embeddable.setup, + licensing: licensingMock.createSetup(), }); return { diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts index 5069b485b198d..015531aab9743 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts @@ -36,16 +36,17 @@ import { } from './custom_time_range_badge'; import { CommonlyUsedRange } from './types'; import { UiActionsServiceEnhancements } from './services'; -import { ILicense, LicensingPluginStart } from '../../licensing/public'; +import { ILicense, LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; import { createFlyoutManageDrilldowns } from './drilldowns'; -import { Storage } from '../../../../src/plugins/kibana_utils/public'; +import { createStartServicesGetter, Storage } from '../../../../src/plugins/kibana_utils/public'; interface SetupDependencies { embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. uiActions: UiActionsSetup; + licensing: LicensingPluginSetup; } -interface StartDependencies { +export interface StartDependencies { embeddable: EmbeddableStart; uiActions: UiActionsStart; licensing: LicensingPluginStart; @@ -70,23 +71,30 @@ declare module '../../../../src/plugins/ui_actions/public' { export class AdvancedUiActionsPublicPlugin implements Plugin { - readonly licenceInfo = new BehaviorSubject(undefined); + readonly licenseInfo = new BehaviorSubject(undefined); private getLicenseInfo(): ILicense { - if (!this.licenceInfo.getValue()) { + if (!this.licenseInfo.getValue()) { throw new Error( - 'AdvancedUiActionsPublicPlugin: Licence is not ready! Licence becomes available only after setup.' + 'AdvancedUiActionsPublicPlugin: License is not ready! License becomes available only after setup.' ); } - return this.licenceInfo.getValue()!; + return this.licenseInfo.getValue()!; } - private readonly enhancements = new UiActionsServiceEnhancements({ - getLicenseInfo: () => this.getLicenseInfo(), - }); + private enhancements?: UiActionsServiceEnhancements; private subs: Subscription[] = []; constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { uiActions }: SetupDependencies): SetupContract { + public setup( + core: CoreSetup, + { uiActions, licensing }: SetupDependencies + ): SetupContract { + const startServices = createStartServicesGetter(core.getStartServices); + this.enhancements = new UiActionsServiceEnhancements({ + getLicense: () => this.getLicenseInfo(), + featureUsageSetup: licensing.featureUsage, + getFeatureUsageStart: () => startServices().plugins.licensing.featureUsage, + }); return { ...uiActions, ...this.enhancements, @@ -94,7 +102,7 @@ export class AdvancedUiActionsPublicPlugin } public start(core: CoreStart, { uiActions, licensing }: StartDependencies): StartContract { - this.subs.push(licensing.license$.subscribe(this.licenceInfo)); + this.subs.push(licensing.license$.subscribe(this.licenseInfo)); const dateFormat = core.uiSettings.get('dateFormat') as string; const commonlyUsedRanges = core.uiSettings.get( @@ -117,9 +125,9 @@ export class AdvancedUiActionsPublicPlugin return { ...uiActions, - ...this.enhancements, + ...this.enhancements!, FlyoutManageDrilldowns: createFlyoutManageDrilldowns({ - actionFactories: this.enhancements.getActionFactories(), + actionFactories: this.enhancements!.getActionFactories(), getTrigger: (triggerId: TriggerId) => uiActions.getTrigger(triggerId), storage: new Storage(window?.localStorage), toastService: core.notifications.toasts, diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts index 08823833b9af2..3a0b65d2ed844 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts @@ -4,11 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UiActionsServiceEnhancements } from './ui_actions_service_enhancements'; +import { + UiActionsServiceEnhancements, + UiActionsServiceEnhancementsParams, +} from './ui_actions_service_enhancements'; import { ActionFactoryDefinition, ActionFactory } from '../dynamic_actions'; import { licensingMock } from '../../../licensing/public/mocks'; -const getLicenseInfo = () => licensingMock.createLicense(); +const deps: UiActionsServiceEnhancementsParams = { + getLicense: () => licensingMock.createLicense(), + featureUsageSetup: licensingMock.createSetup().featureUsage, + getFeatureUsageStart: () => licensingMock.createStart().featureUsage, +}; describe('UiActionsService', () => { describe('action factories', () => { @@ -34,7 +41,7 @@ describe('UiActionsService', () => { }; test('.getActionFactories() returns empty array if no action factories registered', () => { - const service = new UiActionsServiceEnhancements({ getLicenseInfo }); + const service = new UiActionsServiceEnhancements(deps); const factories = service.getActionFactories(); @@ -42,7 +49,7 @@ describe('UiActionsService', () => { }); test('can register and retrieve an action factory', () => { - const service = new UiActionsServiceEnhancements({ getLicenseInfo }); + const service = new UiActionsServiceEnhancements(deps); service.registerActionFactory(factoryDefinition1); @@ -53,7 +60,7 @@ describe('UiActionsService', () => { }); test('can retrieve all action factories', () => { - const service = new UiActionsServiceEnhancements({ getLicenseInfo }); + const service = new UiActionsServiceEnhancements(deps); service.registerActionFactory(factoryDefinition1); service.registerActionFactory(factoryDefinition2); @@ -67,7 +74,7 @@ describe('UiActionsService', () => { }); test('throws when retrieving action factory that does not exist', () => { - const service = new UiActionsServiceEnhancements({ getLicenseInfo }); + const service = new UiActionsServiceEnhancements(deps); service.registerActionFactory(factoryDefinition1); @@ -77,7 +84,7 @@ describe('UiActionsService', () => { }); test('isCompatible from definition is used on registered factory', async () => { - const service = new UiActionsServiceEnhancements({ getLicenseInfo }); + const service = new UiActionsServiceEnhancements(deps); service.registerActionFactory({ ...factoryDefinition1, @@ -88,5 +95,27 @@ describe('UiActionsService', () => { service.getActionFactory(factoryDefinition1.id).isCompatible({ triggers: [] }) ).resolves.toBe(false); }); + + describe('registerFeature for licensing', () => { + const spy = jest.spyOn(deps.featureUsageSetup, 'register'); + beforeEach(() => { + spy.mockClear(); + }); + test('registerFeature is not called if no license requirements', () => { + const service = new UiActionsServiceEnhancements(deps); + service.registerActionFactory(factoryDefinition1); + expect(spy).not.toBeCalled(); + }); + + test('registerFeature is called if has license requirements', () => { + const service = new UiActionsServiceEnhancements(deps); + service.registerActionFactory({ + ...factoryDefinition1, + minimalLicense: 'gold', + licenseFeatureName: 'a name', + }); + expect(spy).toBeCalledWith('a name', 'gold'); + }); + }); }); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index 9575329514835..b8086c16f5e71 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -13,19 +13,22 @@ import { import { DrilldownDefinition } from '../drilldowns'; import { ILicense } from '../../../licensing/common/types'; import { TriggerContextMapping, TriggerId } from '../../../../../src/plugins/ui_actions/public'; +import { LicensingPluginSetup, LicensingPluginStart } from '../../../licensing/public'; export interface UiActionsServiceEnhancementsParams { readonly actionFactories?: ActionFactoryRegistry; - readonly getLicenseInfo: () => ILicense; + readonly getLicense: () => ILicense; + readonly featureUsageSetup: LicensingPluginSetup['featureUsage']; + readonly getFeatureUsageStart: () => LicensingPluginStart['featureUsage']; } export class UiActionsServiceEnhancements { protected readonly actionFactories: ActionFactoryRegistry; - protected readonly getLicenseInfo: () => ILicense; + protected readonly deps: Omit; - constructor({ actionFactories = new Map(), getLicenseInfo }: UiActionsServiceEnhancementsParams) { + constructor({ actionFactories = new Map(), ...deps }: UiActionsServiceEnhancementsParams) { this.actionFactories = actionFactories; - this.getLicenseInfo = getLicenseInfo; + this.deps = deps; } /** @@ -51,9 +54,10 @@ export class UiActionsServiceEnhancements { SupportedTriggers, FactoryContext, ActionContext - >(definition, this.getLicenseInfo); + >(definition, this.deps); this.actionFactories.set(actionFactory.id, actionFactory as ActionFactory); + this.registerFeatureUsage(definition); }; public readonly getActionFactory = (actionFactoryId: string): ActionFactory => { @@ -94,6 +98,7 @@ export class UiActionsServiceEnhancements { execute, getHref, minimalLicense, + licenseFeatureName, supportedTriggers, isCompatible, }: DrilldownDefinition): void => { @@ -105,6 +110,7 @@ export class UiActionsServiceEnhancements { > = { id: factoryId, minimalLicense, + licenseFeatureName, order, CollectConfig, createConfig, @@ -128,4 +134,19 @@ export class UiActionsServiceEnhancements { this.registerActionFactory(actionFactory); }; + + private registerFeatureUsage = (definition: ActionFactoryDefinition): void => { + if (!definition.minimalLicense || !definition.licenseFeatureName) return; + + // Intentionally don't wait for response because + // happens in setup phase and has to be sync + this.deps.featureUsageSetup + .register(definition.licenseFeatureName, definition.minimalLicense) + .catch(() => { + // eslint-disable-next-line no-console + console.warn( + `ActionFactory [actionFactory.id = ${definition.id}] fail to register feature for featureUsage.` + ); + }); + }; } diff --git a/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js b/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js index 1e3ab0d96b81c..bf43167a3ae51 100644 --- a/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js +++ b/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js @@ -9,8 +9,5 @@ import { join } from 'path'; // eslint-disable-next-line require('@kbn/storybook').runStorybookCli({ name: 'ui_actions_enhanced', - storyGlobs: [ - join(__dirname, '..', 'public', 'components', '**', '*.story.tsx'), - join(__dirname, '..', 'public', 'drilldowns', 'components', '**', '*.story.tsx'), - ], + storyGlobs: [join(__dirname, '..', 'public', '**', '*.story.tsx')], }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 9ed453d286285..568b0873b8dbb 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest } from 'kibana/server'; import moment from 'moment'; import { schema } from '@kbn/config-schema'; -import { ILegacyScopedClusterClient } from 'kibana/server'; import { updateState } from './common'; import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; import { commonStateTranslations, durationAnomalyTranslations } from './translations'; @@ -36,13 +36,11 @@ export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Pi const getAnomalies = async ( plugins: UptimeCorePlugins, - mlClusterClient: ILegacyScopedClusterClient, params: Record, lastCheckedAt: string ) => { - const { getAnomaliesTableData } = plugins.ml.resultsServiceProvider(mlClusterClient, { - params: 'DummyKibanaRequest', - } as any); + const fakeRequest = {} as KibanaRequest; + const { getAnomaliesTableData } = plugins.ml.resultsServiceProvider(fakeRequest); return await getAnomaliesTableData( [getMLJobId(params.monitorId)], @@ -82,23 +80,12 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory = (_server, _li producer: 'uptime', async executor(options) { const { - services: { - alertInstanceFactory, - callCluster, - savedObjectsClient, - getLegacyScopedClusterClient, - }, + services: { alertInstanceFactory, callCluster, savedObjectsClient }, state, params, } = options; - const { anomalies } = - (await getAnomalies( - plugins, - getLegacyScopedClusterClient(plugins.ml.mlClient), - params, - state.lastCheckedAt - )) ?? {}; + const { anomalies } = (await getAnomalies(plugins, params, state.lastCheckedAt)) ?? {}; const foundAnomalies = anomalies?.length > 0; 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 b927b563eb54a..78ca2af12ec3f 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 @@ -28,5 +28,8 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./alerts_space1')); loadTestFile(require.resolve('./alerts_default_space')); loadTestFile(require.resolve('./builtin_alert_types')); + + // note that this test will destroy existing spaces + loadTestFile(require.resolve('./migrations')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts new file mode 100644 index 0000000000000..81f7c8c97ba8c --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.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 expect from '@kbn/expect'; +import { getUrlPrefix } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('alerts'); + }); + + after(async () => { + await esArchiver.unload('alerts'); + }); + + it('7.10.0 migrates the `alerting` consumer to be the `alerts`', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` + ); + + expect(response.status).to.eql(200); + expect(response.body.consumer).to.equal('alerts'); + }); + + it('7.10.0 migrates the `metrics` consumer to be the `infrastructure`', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-fdf248d5f2a4` + ); + + expect(response.status).to.eql(200); + expect(response.body.consumer).to.equal('infrastructure'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/update.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/update.ts index f4964308cd8c9..7aa5c180c5a02 100644 --- a/x-pack/test/api_integration/apis/ml/data_frame_analytics/update.ts +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/update.ts @@ -258,7 +258,7 @@ export default ({ getService }: FtrProviderContext) => { description: 'Not found', }; const id = `${jobId}_invalid`; - const message = `[resource_not_found_exception] No known data frame analytics with id [${id}]`; + const message = 'resource_not_found_exception'; const { body } = await supertest .post(`/api/ml/data_frame/analytics/${id}/_update`) diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_histograms.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_histograms.ts index 299f5f93fd281..a5969bdffbf8a 100644 --- a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_histograms.ts +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_histograms.ts @@ -60,8 +60,7 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { statusCode: 404, error: 'Not Found', - message: - '[index_not_found_exception] no such index [ft_farequote_not_exists], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exists" & index_uuid="_na_" & index="ft_farequote_not_exists" }', + message: 'index_not_found_exception', }, }, }; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts index 5795eac9637b1..4ec8ae0429a6b 100644 --- a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts @@ -152,8 +152,7 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { statusCode: 404, error: 'Not Found', - message: - '[index_not_found_exception] no such index [ft_farequote_not_exists], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exists" & index_uuid="_na_" & index="ft_farequote_not_exists" }', + message: 'index_not_found_exception', }, }, }; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts index fa83807be161a..af2c53c577253 100644 --- a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts @@ -116,8 +116,7 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { statusCode: 404, error: 'Not Found', - message: - '[index_not_found_exception] no such index [ft_farequote_not_exist], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exist" & index_uuid="_na_" & index="ft_farequote_not_exist" }', + message: 'index_not_found_exception', }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts index 627d9454beeb6..7dbb4a9d03e4f 100644 --- a/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts +++ b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts @@ -78,8 +78,7 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { statusCode: 404, error: 'Not Found', - message: - '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }', + message: 'index_not_found_exception', }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts index b1c086ddbb456..6588d4df570b7 100644 --- a/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts +++ b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts @@ -81,8 +81,7 @@ export default ({ getService }: FtrProviderContext) => { responseBody: { statusCode: 404, error: 'Not Found', - message: - '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }', + message: 'index_not_found_exception', }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/filters/create_filters.ts b/x-pack/test/api_integration/apis/ml/filters/create_filters.ts index dfec7798ffc0c..598fab2cb7fa6 100644 --- a/x-pack/test/api_integration/apis/ml/filters/create_filters.ts +++ b/x-pack/test/api_integration/apis/ml/filters/create_filters.ts @@ -73,7 +73,7 @@ export default ({ getService }: FtrProviderContext) => { responseCode: 400, responseBody: { error: 'Bad Request', - message: 'Invalid filter_id', + message: 'status_exception', }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/filters/get_filters.ts b/x-pack/test/api_integration/apis/ml/filters/get_filters.ts index 5d7900ea5e9d9..4a3589634ded9 100644 --- a/x-pack/test/api_integration/apis/ml/filters/get_filters.ts +++ b/x-pack/test/api_integration/apis/ml/filters/get_filters.ts @@ -91,7 +91,7 @@ export default ({ getService }: FtrProviderContext) => { .set(COMMON_REQUEST_HEADERS) .expect(400); expect(body.error).to.eql('Bad Request'); - expect(body.message).to.contain('Unable to find filter'); + expect(body.message).to.contain('resource_not_found_exception'); }); }); }; diff --git a/x-pack/test/api_integration/apis/ml/filters/update_filters.ts b/x-pack/test/api_integration/apis/ml/filters/update_filters.ts index fbbb94d54c035..6f421ad120b51 100644 --- a/x-pack/test/api_integration/apis/ml/filters/update_filters.ts +++ b/x-pack/test/api_integration/apis/ml/filters/update_filters.ts @@ -111,7 +111,7 @@ export default ({ getService }: FtrProviderContext) => { .send(updateFilterRequestBody) .expect(400); - expect(body.message).to.contain('No filter with id'); + expect(body.message).to.contain('resource_not_found_exception'); }); }); }; diff --git a/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts b/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts index 5b9c5393e81d9..0b7f9cf927d26 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/close_jobs.ts @@ -47,8 +47,8 @@ export default ({ getService }: FtrProviderContext) => { responseCode: 200, responseBody: { - [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: false, error: { statusCode: 409 } }, - [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: false, error: { statusCode: 409 } }, + [SINGLE_METRIC_JOB_CONFIG.job_id]: { closed: false, error: { status: 409 } }, + [MULTI_METRIC_JOB_CONFIG.job_id]: { closed: false, error: { status: 409 } }, }, }, }, @@ -162,9 +162,7 @@ export default ({ getService }: FtrProviderContext) => { expectedRspJobIds.forEach((id) => { expect(body[id].closed).to.eql(testData.expected.responseBody[id].closed); - expect(body[id].error.statusCode).to.eql( - testData.expected.responseBody[id].error.statusCode - ); + expect(body[id].error.status).to.eql(testData.expected.responseBody[id].error.status); }); // ensure jobs are still open diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts rename to x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts index 29ead0db1c634..c300412c393bc 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts @@ -22,7 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); - describe('Dashboard Drilldowns', function () { + describe('Dashboard to dashboard drilldown', function () { before(async () => { log.debug('Dashboard Drilldowns:initTests'); await PageObjects.common.navigateToApp('dashboard'); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts new file mode 100644 index 0000000000000..12de29c4fde10 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts @@ -0,0 +1,96 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +const DRILLDOWN_TO_DISCOVER_URL = 'Go to discover'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); + const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); + const PageObjects = getPageObjects(['dashboard', 'common', 'header', 'timePicker', 'discover']); + const log = getService('log'); + const browser = getService('browser'); + const testSubjects = getService('testSubjects'); + + describe('Dashboard to URL drilldown', function () { + before(async () => { + log.debug('Dashboard to URL:initTests'); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + }); + + it('should create dashboard to URL drilldown and use it to navigate to discover', async () => { + await PageObjects.dashboard.gotoDashboardEditMode( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + + // create drilldown + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); + await dashboardDrilldownPanelActions.clickCreateDrilldown(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); + + const urlTemplate = `{{kibanaUrl}}/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'{{event.from}}',to:'{{event.to}}'))&_a=(columns:!(_source),filters:{{rison context.panel.filters}},index:'{{context.panel.indexPatternId}}',interval:auto,query:(language:{{context.panel.query.language}},query:'{{context.panel.query.query}}'),sort:!())`; + + await dashboardDrilldownsManage.fillInDashboardToURLDrilldownWizard({ + drilldownName: DRILLDOWN_TO_DISCOVER_URL, + destinationURLTemplate: urlTemplate, + trigger: 'SELECT_RANGE_TRIGGER', + }); + await dashboardDrilldownsManage.saveChanges(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(2); + + // save dashboard, navigate to view mode + await PageObjects.dashboard.saveDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + { + saveAsNew: false, + waitDialogIsClosed: true, + } + ); + + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_DISCOVER_URL); + + await PageObjects.discover.waitForDiscoverAppOnScreen(); + + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + }); + }); + + // utils which shouldn't be a part of test flow, but also too specific to be moved to pageobject or service + async function brushAreaChart() { + const areaChart = await testSubjects.find('visualizationLoader'); + expect(await areaChart.getAttribute('data-title')).to.be('Visualization漢字 AreaChart'); + await browser.dragAndDrop( + { + location: areaChart, + offset: { + x: -100, + y: 0, + }, + }, + { + location: areaChart, + offset: { + x: 100, + y: 0, + }, + } + ); + } +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts index ff604b18e1d51..57454f50266da 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts @@ -22,7 +22,8 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { await esArchiver.unload('dashboard/drilldowns'); }); - loadTestFile(require.resolve('./dashboard_drilldowns')); + loadTestFile(require.resolve('./dashboard_to_dashboard_drilldown')); + loadTestFile(require.resolve('./dashboard_to_url_drilldown')); loadTestFile(require.resolve('./explore_data_panel_action')); // Disabled for now as it requires xpack.discoverEnhanced.actions.exploreDataInChart.enabled diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts index 18a7a4b26a752..3fbe101d4ff19 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; @@ -433,9 +432,7 @@ export default function ({ getService }: FtrProviderContext) { 'job creation displays the created job in the job list' ); await ml.jobTable.refreshJobList(); - await ml.jobTable.filterWithSearchString(testData.jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === testData.jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' @@ -651,9 +648,7 @@ export default function ({ getService }: FtrProviderContext) { 'job cloning displays the created job in the job list' ); await ml.jobTable.refreshJobList(); - await ml.jobTable.filterWithSearchString(testData.jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === testData.jobIdClone)).to.have.length(1); + await ml.jobTable.filterWithSearchString(testData.jobIdClone, 1); await ml.testExecution.logTestStep( 'job cloning displays details for the created job in the job list' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts index 9e48c71ab0eba..5353f53e74d0b 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; @@ -62,9 +61,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === JOB_CONFIG.job_id)).to.have.length(1); + await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id, 1); await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts index cfbebd478fcb8..4fdbda6e54893 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; @@ -86,9 +85,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('open job in anomaly explorer'); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === testData.jobConfig.job_id)).to.have.length(1); + await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id, 1); await ml.jobTable.clickOpenJobInAnomalyExplorerButton(testData.jobConfig.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts index c410aff292ffa..2524d0486171b 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../plugins/ml/common/constants/categorization_job'; @@ -202,9 +201,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' @@ -320,9 +317,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobIdClone)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobIdClone, 1); await ml.testExecution.logTestStep( 'job cloning displays details for the created job in the job list' @@ -353,9 +348,7 @@ export default function ({ getService }: FtrProviderContext) { 'job deletion does not display the deleted job in the job list any more' ); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobIdClone)).to.have.length(0); + await ml.jobTable.filterWithSearchString(jobIdClone, 0); await ml.testExecution.logTestStep( 'job deletion does not have results for the deleted job any more' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts index 22b4c4a1fdfe3..af30946ee08ce 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; @@ -315,9 +314,7 @@ export default function ({ getService }: FtrProviderContext) { 'job creation displays the created job in the job list' ); await ml.jobTable.refreshJobList(); - await ml.jobTable.filterWithSearchString(testData.jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === testData.jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts index 8702cfd734454..5324890b269bc 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; @@ -205,9 +204,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' @@ -340,9 +337,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobIdClone)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobIdClone, 1); await ml.testExecution.logTestStep( 'job cloning displays details for the created job in the job list' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts index 3ec78eccf3de4..4797334ee57af 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; @@ -231,9 +230,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' @@ -377,9 +374,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobIdClone)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobIdClone, 1); await ml.testExecution.logTestStep( 'job cloning displays details for the created job in the job list' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts index 170b88efd70f5..ea3a42e2f27c8 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; @@ -410,9 +409,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(testData.jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === testData.jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts index ba5628661bfc2..89612e51eee13 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; @@ -184,9 +183,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' @@ -303,9 +300,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobIdClone)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobIdClone, 1); await ml.testExecution.logTestStep( 'job cloning displays details for the created job in the job list' @@ -336,9 +331,7 @@ export default function ({ getService }: FtrProviderContext) { 'job deletion does not display the deleted job in the job list any more' ); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobIdClone)).to.have.length(0); + await ml.jobTable.filterWithSearchString(jobIdClone, 0); await ml.testExecution.logTestStep( 'job deletion does not have results for the deleted job any more' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts index e1ab3f8e092c3..1dc4708c57dbc 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; @@ -60,9 +59,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('open job in single metric viewer'); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === JOB_CONFIG.job_id)).to.have.length(1); + await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id, 1); await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 6beefaafa3792..e12c853a0be83 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; @@ -147,13 +146,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the created job in the analytics table'); await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); - await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId); - const rows = await ml.dataFrameAnalyticsTable.parseAnalyticsTable(); - const filteredRows = rows.filter((row) => row.id === testData.jobId); - expect(filteredRows).to.have.length( - 1, - `Filtered analytics table should have 1 row for job id '${testData.jobId}' (got matching items '${filteredRows}')` - ); + await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( 'displays details for the created job in the analytics table' @@ -208,9 +201,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.api.assertIndicesNotEmpty(testData.destinationIndex); await ml.testExecution.logTestStep('displays the results view for created job'); - await ml.dataFrameAnalyticsTable.openResultsView(); - await ml.dataFrameAnalytics.assertClassificationEvaluatePanelElementsExists(); - await ml.dataFrameAnalytics.assertClassificationTablePanelExists(); + await ml.dataFrameAnalyticsTable.openResultsView(testData.jobId); + await ml.dataFrameAnalyticsResults.assertClassificationEvaluatePanelElementsExists(); + await ml.dataFrameAnalyticsResults.assertClassificationTablePanelExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); }); }); } diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts index 5494f2f963d37..532de930bc1a1 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts @@ -148,7 +148,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToDataFrameAnalytics(); await ml.dataFrameAnalyticsTable.waitForAnalyticsToLoad(); - await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.job.id as string); + await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.job.id as string, 1); await ml.dataFrameAnalyticsTable.cloneJob(testData.job.id as string); }); @@ -217,13 +217,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.dataFrameAnalyticsCreation.navigateToJobManagementPage(); await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); - await ml.dataFrameAnalyticsTable.filterWithSearchString(cloneJobId); - const rows = await ml.dataFrameAnalyticsTable.parseAnalyticsTable(); - const filteredRows = rows.filter((row) => row.id === cloneJobId); - expect(filteredRows).to.have.length( - 1, - `Filtered analytics table should have 1 row for job id '${cloneJobId}' (got matching items '${filteredRows}')` - ); + await ml.dataFrameAnalyticsTable.filterWithSearchString(cloneJobId, 1); }); }); } diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index e4bc7b940aaea..b5b0f4c94f262 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; @@ -163,13 +162,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the created job in the analytics table'); await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); - await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId); - const rows = await ml.dataFrameAnalyticsTable.parseAnalyticsTable(); - const filteredRows = rows.filter((row) => row.id === testData.jobId); - expect(filteredRows).to.have.length( - 1, - `Filtered analytics table should have 1 row for job id '${testData.jobId}' (got matching items '${filteredRows}')` - ); + await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( 'displays details for the created job in the analytics table' @@ -224,8 +217,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.api.assertIndicesNotEmpty(testData.destinationIndex); await ml.testExecution.logTestStep('displays the results view for created job'); - await ml.dataFrameAnalyticsTable.openResultsView(); - await ml.dataFrameAnalytics.assertOutlierTablePanelExists(); + await ml.dataFrameAnalyticsTable.openResultsView(testData.jobId); + await ml.dataFrameAnalyticsResults.assertOutlierTablePanelExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); }); }); } diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index af9c5417e4826..c58220f2d1ad2 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../../ftr_provider_context'; @@ -147,13 +146,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the created job in the analytics table'); await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); - await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId); - const rows = await ml.dataFrameAnalyticsTable.parseAnalyticsTable(); - const filteredRows = rows.filter((row) => row.id === testData.jobId); - expect(filteredRows).to.have.length( - 1, - `Filtered analytics table should have 1 row for job id '${testData.jobId}' (got matching items '${filteredRows}')` - ); + await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( 'displays details for the created job in the analytics table' @@ -208,9 +201,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.api.assertIndicesNotEmpty(testData.destinationIndex); await ml.testExecution.logTestStep('displays the results view for created job'); - await ml.dataFrameAnalyticsTable.openResultsView(); - await ml.dataFrameAnalytics.assertRegressionEvaluatePanelElementsExists(); - await ml.dataFrameAnalytics.assertRegressionTablePanelExists(); + await ml.dataFrameAnalyticsTable.openResultsView(testData.jobId); + await ml.dataFrameAnalyticsResults.assertRegressionEvaluatePanelElementsExists(); + await ml.dataFrameAnalyticsResults.assertRegressionTablePanelExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); }); }); } diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/index.ts index 2d8aac3b8dddf..e224f5c8bb128 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/index.ts @@ -20,10 +20,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { after(async () => { await ml.securityCommon.cleanMlUsers(); await ml.securityCommon.cleanMlRoles(); - await ml.testResources.deleteSavedSearches(); await ml.testResources.deleteDashboards(); - await ml.testResources.deleteIndexPatternByTitle('ft_farequote'); await ml.testResources.deleteIndexPatternByTitle('ft_ecommerce'); await ml.testResources.deleteIndexPatternByTitle('ft_categorization'); @@ -31,7 +29,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.deleteIndexPatternByTitle('ft_bank_marketing'); await ml.testResources.deleteIndexPatternByTitle('ft_ihp_outlier'); await ml.testResources.deleteIndexPatternByTitle('ft_egs_regression'); - await esArchiver.unload('ml/farequote'); await esArchiver.unload('ml/ecommerce'); await esArchiver.unload('ml/categorization'); @@ -39,11 +36,12 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('ml/bm_classification'); await esArchiver.unload('ml/ihp_outlier'); await esArchiver.unload('ml/egs_regression'); - await ml.testResources.resetKibanaTimeZone(); + await ml.securityUI.logout(); }); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./permissions')); loadTestFile(require.resolve('./pages')); loadTestFile(require.resolve('./anomaly_detection')); loadTestFile(require.resolve('./data_visualizer')); diff --git a/x-pack/test/functional/apps/ml/pages.ts b/x-pack/test/functional/apps/ml/pages.ts index 5d084d5abe11e..27b61a7dbc0f8 100644 --- a/x-pack/test/functional/apps/ml/pages.ts +++ b/x-pack/test/functional/apps/ml/pages.ts @@ -17,7 +17,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('loads the ML pages', async () => { - await ml.testExecution.logTestStep('home'); + await ml.testExecution.logTestStep('loads the ML home page'); await ml.navigation.navigateToMl(); await ml.testExecution.logTestStep('loads the overview page'); @@ -34,10 +34,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('loads the settings page'); await ml.navigation.navigateToSettings(); - await ml.settings.assertSettingsManageCalendarsLinkExists(); - await ml.settings.assertSettingsCreateCalendarLinkExists(); - await ml.settings.assertSettingsManageFilterListsLinkExists(); - await ml.settings.assertSettingsCreateFilterListLinkExists(); + await ml.settings.assertManageCalendarsLinkExists(); + await ml.settings.assertCreateCalendarLinkExists(); + await ml.settings.assertManageFilterListsLinkExists(); + await ml.settings.assertCreateFilterListLinkExists(); await ml.testExecution.logTestStep('loads the data frame analytics page'); await ml.navigation.navigateToDataFrameAnalytics(); diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts new file mode 100644 index 0000000000000..eed7489b09fe6 --- /dev/null +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -0,0 +1,480 @@ +/* + * 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 path from 'path'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +import { USER } from '../../../services/ml/security_common'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + const testUsers = [USER.ML_POWERUSER, USER.ML_POWERUSER_SPACES]; + + describe('for user with full ML access', function () { + this.tags(['skipFirefox', 'mlqa']); + + describe('with no data loaded', function () { + for (const user of testUsers) { + describe(`(${user})`, function () { + before(async () => { + await ml.securityUI.loginAs(user); + await ml.api.cleanMlIndices(); + }); + + after(async () => { + await ml.securityUI.logout(); + }); + + it('should display the ML file data vis link on the Kibana home page', async () => { + await ml.testExecution.logTestStep('should load the Kibana home page'); + await ml.navigation.navigateToKibanaHome(); + + await ml.testExecution.logTestStep('should display the ML file data vis link'); + await ml.commonUI.assertKibanaHomeFileDataVisLinkExists(); + }); + + it('should display the ML entry in Kibana app menu', async () => { + await ml.testExecution.logTestStep('should open the Kibana app menu'); + await ml.navigation.openKibanaNav(); + + await ml.testExecution.logTestStep('should display the ML nav link'); + await ml.navigation.assertKibanaNavMLEntryExists(); + }); + + it('should display elements on ML Overview page correctly', async () => { + await ml.testExecution.logTestStep('should load the ML overview page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToOverview(); + + await ml.testExecution.logTestStep('should display enabled AD create job button'); + await ml.overviewPage.assertADCreateJobButtonExists(); + await ml.overviewPage.assertADCreateJobButtonEnabled(true); + + await ml.testExecution.logTestStep('should display enabled DFA create job button'); + await ml.overviewPage.assertDFACreateJobButtonExists(); + await ml.overviewPage.assertDFACreateJobButtonEnabled(true); + }); + }); + } + }); + + describe('with data loaded', function () { + const adJobId = 'fq_single_permission'; + const dfaJobId = 'iph_outlier_permission'; + const calendarId = 'calendar_permission'; + const eventDescription = 'calendar_event_permission'; + const filterId = 'filter_permission'; + const filterItems = ['filter_item_permission']; + + const ecIndexPattern = 'ft_module_sample_ecommerce'; + const ecExpectedTotalCount = 287; + const ecExpectedFieldPanelCount = 2; + const ecExpectedModuleId = 'sample_data_ecommerce'; + + const uploadFilePath = path.join( + __dirname, + '..', + 'data_visualizer', + 'files_to_import', + 'artificial_server_log' + ); + const expectedUploadFileTitle = 'artificial_server_log'; + + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await esArchiver.loadIfNeeded('ml/ihp_outlier'); + await esArchiver.loadIfNeeded('ml/module_sample_ecommerce'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded(ecIndexPattern, 'order_date'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.api.createAndRunAnomalyDetectionLookbackJob( + ml.commonConfig.getADFqMultiMetricJobConfig(adJobId), + ml.commonConfig.getADFqDatafeedConfig(adJobId) + ); + + await ml.api.createAndRunDFAJob( + ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(dfaJobId) + ); + + await ml.api.createCalendar(calendarId, { + calendar_id: calendarId, + job_ids: [], + description: 'Test calendar', + }); + await ml.api.createCalendarEvents(calendarId, [ + { + description: eventDescription, + start_time: 1513641600000, + end_time: 1513728000000, + }, + ]); + + await ml.api.createFilter(filterId, { + description: 'Test filter list', + items: filterItems, + }); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.api.deleteIndices(`user-${dfaJobId}`); + await ml.api.deleteCalendar(calendarId); + await ml.api.deleteFilter(filterId); + }); + + for (const user of testUsers) { + describe(`(${user})`, function () { + before(async () => { + await ml.securityUI.loginAs(user); + }); + + after(async () => { + await ml.securityUI.logout(); + }); + + it('should display elements on Anomaly Detection page correctly', async () => { + await ml.testExecution.logTestStep('should load the AD job management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToAnomalyDetection(); + + await ml.testExecution.logTestStep('should display the stats bar and the AD job table'); + await ml.jobManagement.assertJobStatsBarExists(); + await ml.jobManagement.assertJobTableExists(); + + await ml.testExecution.logTestStep('should display an enabled "Create job" button'); + await ml.jobManagement.assertCreateNewJobButtonExists(); + await ml.jobManagement.assertCreateNewJobButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the AD job in the list'); + await ml.jobTable.filterWithSearchString(adJobId, 1); + + await ml.testExecution.logTestStep('should display enabled AD job result links'); + await ml.jobTable.assertJobActionSingleMetricViewerButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionAnomalyExplorerButtonEnabled(adJobId, true); + + await ml.testExecution.logTestStep('should display enabled AD job row action buttons'); + await ml.jobTable.assertJobActionsMenuButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionStartDatafeedButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionCloneJobButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionEditJobButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionDeleteJobButtonEnabled(adJobId, true); + + await ml.testExecution.logTestStep('should select the job'); + await ml.jobTable.selectJobRow(adJobId); + + await ml.testExecution.logTestStep('should display enabled multi select result links'); + await ml.jobTable.assertMultiSelectActionSingleMetricViewerButtonEnabled(true); + await ml.jobTable.assertMultiSelectActionAnomalyExplorerButtonEnabled(true); + + await ml.testExecution.logTestStep( + 'should display enabled multi select action buttons' + ); + await ml.jobTable.assertMultiSelectManagementActionsButtonEnabled(true); + await ml.jobTable.assertMultiSelectStartDatafeedActionButtonEnabled(true); + await ml.jobTable.assertMultiSelectDeleteJobActionButtonEnabled(true); + await ml.jobTable.deselectJobRow(adJobId); + }); + + it('should display elements on Single Metric Viewer page correctly', async () => { + await ml.testExecution.logTestStep('should open AD job in the single metric viewer'); + await ml.jobTable.clickOpenJobInSingleMetricViewerButton(adJobId); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + + await ml.testExecution.logTestStep('should pre-fill the AD job selection'); + await ml.jobSelection.assertJobSelection([adJobId]); + + await ml.testExecution.logTestStep('should pre-fill the detector input'); + await ml.singleMetricViewer.assertDetectorInputExsist(); + await ml.singleMetricViewer.assertDetectorInputValue('0'); + + await ml.testExecution.logTestStep('should input the airline entity value'); + await ml.singleMetricViewer.assertEntityInputExsist('airline'); + await ml.singleMetricViewer.assertEntityInputSelection('airline', []); + await ml.singleMetricViewer.selectEntityValue('airline', 'AAL'); + + await ml.testExecution.logTestStep('should display the chart'); + await ml.singleMetricViewer.assertChartExsist(); + + await ml.testExecution.logTestStep('should display the annotations section'); + await ml.singleMetricViewer.assertAnnotationsExists('loaded'); + + await ml.testExecution.logTestStep('should display the anomalies table with entries'); + await ml.anomaliesTable.assertTableExists(); + await ml.anomaliesTable.assertTableNotEmpty(); + + await ml.testExecution.logTestStep('should display enabled anomaly row action buttons'); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonExists(0); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonEnabled(0, true); + await ml.anomaliesTable.assertAnomalyActionConfigureRulesButtonEnabled(0, true); + + await ml.testExecution.logTestStep( + 'should display the forecast modal with enabled run button' + ); + await ml.singleMetricViewer.assertForecastButtonExists(); + await ml.singleMetricViewer.assertForecastButtonEnabled(true); + await ml.singleMetricViewer.openForecastModal(); + await ml.singleMetricViewer.assertForecastModalRunButtonEnabled(true); + await ml.singleMetricViewer.closeForecastModal(); + }); + + it('should display elements on Anomaly Explorer page correctly', async () => { + await ml.testExecution.logTestStep('should open AD job in the anomaly explorer'); + await ml.singleMetricViewer.openAnomalyExplorer(); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + + await ml.testExecution.logTestStep('should pre-fill the AD job selection'); + await ml.jobSelection.assertJobSelection([adJobId]); + + await ml.testExecution.logTestStep('should display the influencers list'); + await ml.anomalyExplorer.assertInfluencerListExists(); + await ml.anomalyExplorer.assertInfluencerFieldListNotEmpty('airline'); + + await ml.testExecution.logTestStep('should display the swim lanes'); + await ml.anomalyExplorer.assertOverallSwimlaneExists(); + await ml.anomalyExplorer.assertSwimlaneViewByExists(); + + await ml.testExecution.logTestStep('should display the annotations panel'); + await ml.anomalyExplorer.assertAnnotationsPanelExists('loaded'); + + await ml.testExecution.logTestStep('should display the anomalies table with entries'); + await ml.anomaliesTable.assertTableExists(); + await ml.anomaliesTable.assertTableNotEmpty(); + + await ml.testExecution.logTestStep('should display enabled anomaly row action button'); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonExists(0); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonEnabled(0, true); + + await ml.testExecution.logTestStep( + 'should display enabled configure rules action button' + ); + await ml.anomaliesTable.assertAnomalyActionConfigureRulesButtonEnabled(0, true); + + await ml.testExecution.logTestStep('should display enabled view series action button'); + await ml.anomaliesTable.assertAnomalyActionViewSeriesButtonEnabled(0, true); + }); + + it('should display elements on Data Frame Analytics page correctly', async () => { + await ml.testExecution.logTestStep('should load the DFA job management page'); + await ml.navigation.navigateToDataFrameAnalytics(); + + await ml.testExecution.logTestStep( + 'should display the stats bar and the analytics table' + ); + await ml.dataFrameAnalytics.assertAnalyticsStatsBarExists(); + await ml.dataFrameAnalytics.assertAnalyticsTableExists(); + + await ml.testExecution.logTestStep('should display an enabled "Create job" button'); + await ml.dataFrameAnalytics.assertCreateNewAnalyticsButtonExists(); + await ml.dataFrameAnalytics.assertCreateNewAnalyticsButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the DFA job in the list'); + await ml.dataFrameAnalyticsTable.filterWithSearchString(dfaJobId, 1); + + await ml.testExecution.logTestStep( + 'should display enabled DFA job view and action menu' + ); + await ml.dataFrameAnalyticsTable.assertJobRowViewButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJowRowActionsMenuButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJobActionViewButtonEnabled(dfaJobId, true); + + await ml.testExecution.logTestStep('should display enabled DFA job row action buttons'); + await ml.dataFrameAnalyticsTable.assertJobActionStartButtonEnabled(dfaJobId, false); // job already completed + await ml.dataFrameAnalyticsTable.assertJobActionEditButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJobActionCloneButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJobActionDeleteButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.ensureJobActionsMenuClosed(dfaJobId); + }); + + it('should display elements on Data Frame Analytics results view page correctly', async () => { + await ml.testExecution.logTestStep('displays the results view for created job'); + await ml.dataFrameAnalyticsTable.openResultsView(dfaJobId); + await ml.dataFrameAnalyticsResults.assertOutlierTablePanelExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); + }); + + it('should display elements on Data Visualizer home page correctly', async () => { + await ml.testExecution.logTestStep('should load the data visualizer page'); + await ml.navigation.navigateToDataVisualizer(); + + await ml.testExecution.logTestStep( + 'should display the "import data" card with enabled button' + ); + await ml.dataVisualizer.assertDataVisualizerImportDataCardExists(); + await ml.dataVisualizer.assertUploadFileButtonEnabled(true); + + await ml.testExecution.logTestStep( + 'should display the "select index pattern" card with enabled button' + ); + await ml.dataVisualizer.assertDataVisualizerIndexDataCardExists(); + await ml.dataVisualizer.assertSelectIndexButtonEnabled(true); + }); + + it('should display elements on Index Data Visualizer page correctly', async () => { + await ml.testExecution.logTestStep( + 'should load an index into the data visualizer page' + ); + await ml.dataVisualizer.navigateToIndexPatternSelection(); + await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer(ecIndexPattern); + + await ml.testExecution.logTestStep('should display the time range step'); + await ml.dataVisualizerIndexBased.assertTimeRangeSelectorSectionExists(); + + await ml.testExecution.logTestStep('should load data for full time range'); + await ml.dataVisualizerIndexBased.clickUseFullDataButton(ecExpectedTotalCount); + + await ml.testExecution.logTestStep('should display the panels of fields'); + await ml.dataVisualizerIndexBased.assertFieldsPanelsExist(ecExpectedFieldPanelCount); + + await ml.testExecution.logTestStep('should display the actions panel with cards'); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardExists(); + await ml.dataVisualizerIndexBased.assertRecognizerCardExists(ecExpectedModuleId); + }); + + it('should display elements on File Data Visualizer page correctly', async () => { + await ml.testExecution.logTestStep( + 'should load the file data visualizer file selection' + ); + await ml.navigation.navigateToDataVisualizer(); + await ml.dataVisualizer.navigateToFileUpload(); + + await ml.testExecution.logTestStep( + 'should select a file and load visualizer result page' + ); + await ml.dataVisualizerFileBased.selectFile(uploadFilePath); + + await ml.testExecution.logTestStep( + 'should displays components of the file details page' + ); + await ml.dataVisualizerFileBased.assertFileTitle(expectedUploadFileTitle); + await ml.dataVisualizerFileBased.assertFileContentPanelExists(); + await ml.dataVisualizerFileBased.assertSummaryPanelExists(); + await ml.dataVisualizerFileBased.assertFileStatsPanelExists(); + await ml.dataVisualizerFileBased.assertImportButtonEnabled(true); + }); + + it('should display elements on Settings home page correctly', async () => { + await ml.testExecution.logTestStep('should load the settings page'); + await ml.navigation.navigateToSettings(); + + await ml.testExecution.logTestStep('should display enabled calendar controls'); + await ml.settings.assertManageCalendarsLinkExists(); + await ml.settings.assertManageCalendarsLinkEnabled(true); + await ml.settings.assertCreateCalendarLinkExists(); + await ml.settings.assertCreateCalendarLinkEnabled(true); + + await ml.testExecution.logTestStep('should display enabled filter list controls'); + await ml.settings.assertManageFilterListsLinkExists(); + await ml.settings.assertManageFilterListsLinkEnabled(true); + await ml.settings.assertCreateFilterListLinkExists(); + await ml.settings.assertCreateFilterListLinkEnabled(true); + }); + + it('should display elements on Calendar management page correctly', async () => { + await ml.testExecution.logTestStep('should load the calendar management page'); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('should display enabled create calendar button'); + await ml.settingsCalendar.assertCreateCalendarButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the calendar in the list'); + await ml.settingsCalendar.filterWithSearchString(calendarId, 1); + + await ml.testExecution.logTestStep('should enable delete calendar button on selection'); + await ml.settingsCalendar.assertDeleteCalendarButtonEnabled(false); + await ml.settingsCalendar.selectCalendarRow(calendarId); + await ml.settingsCalendar.assertDeleteCalendarButtonEnabled(true); + + await ml.testExecution.logTestStep('should load the calendar edit page'); + await ml.settingsCalendar.openCalendarEditForm(calendarId); + + await ml.testExecution.logTestStep( + 'should display enabled elements of the edit calendar page' + ); + await ml.settingsCalendar.assertApplyToAllJobsSwitchEnabled(true); + await ml.settingsCalendar.assertJobSelectionEnabled(true); + await ml.settingsCalendar.assertJobGroupSelectionEnabled(true); + await ml.settingsCalendar.assertNewEventButtonEnabled(true); + await ml.settingsCalendar.assertImportEventsButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the event in the list'); + await ml.settingsCalendar.assertEventRowExists(eventDescription); + + await ml.testExecution.logTestStep('should display enabled delete event button'); + await ml.settingsCalendar.assertDeleteEventButtonEnabled(eventDescription, true); + }); + + it('should display elements on Filter Lists management page correctly', async () => { + await ml.testExecution.logTestStep('should load the filter list management page'); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToFilterListsManagement(); + + await ml.testExecution.logTestStep('should display enabled create filter list button'); + await ml.settingsFilterList.assertCreateFilterListButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the filter list in the table'); + await ml.settingsFilterList.filterWithSearchString(filterId, 1); + + await ml.testExecution.logTestStep( + 'should enable delete filter list button on selection' + ); + await ml.settingsFilterList.assertDeleteFilterListButtonEnabled(false); + await ml.settingsFilterList.selectFilterListRow(filterId); + await ml.settingsFilterList.assertDeleteFilterListButtonEnabled(true); + + await ml.testExecution.logTestStep('should load the filter list edit page'); + await ml.settingsFilterList.openFilterListEditForm(filterId); + + await ml.testExecution.logTestStep( + 'should display enabled elements of the edit calendar page' + ); + await ml.settingsFilterList.assertEditDescriptionButtonEnabled(true); + await ml.settingsFilterList.assertAddItemButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the filter item in the list'); + await ml.settingsFilterList.assertFilterItemExists(filterItems[0]); + + await ml.testExecution.logTestStep( + 'should enable delete filter item button on selection' + ); + await ml.settingsFilterList.assertDeleteItemButtonEnabled(false); + await ml.settingsFilterList.selectFilterItem(filterItems[0]); + await ml.settingsFilterList.assertDeleteItemButtonEnabled(true); + }); + + it('should display elements on Stack Management ML page correctly', async () => { + await ml.testExecution.logTestStep( + 'should load the stack management with the ML menu item being present' + ); + await ml.navigation.navigateToStackManagement(); + + await ml.testExecution.logTestStep( + 'should load the jobs list page in stack management' + ); + await ml.navigation.navigateToStackManagementJobsListPage(); + + await ml.testExecution.logTestStep('should display the AD job in the list'); + await ml.jobTable.filterWithSearchString(adJobId, 1); + + await ml.testExecution.logTestStep( + 'should load the analytics jobs list page in stack management' + ); + await ml.navigation.navigateToStackManagementJobsListPageAnalyticsTab(); + + await ml.testExecution.logTestStep('should display the DFA job in the list'); + await ml.dataFrameAnalyticsTable.filterWithSearchString(dfaJobId, 1); + }); + }); + } + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/permissions/index.ts b/x-pack/test/functional/apps/ml/permissions/index.ts new file mode 100644 index 0000000000000..2d415b1d094a4 --- /dev/null +++ b/x-pack/test/functional/apps/ml/permissions/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('permissions', function () { + this.tags(['skipFirefox']); + + loadTestFile(require.resolve('./full_ml_access')); + loadTestFile(require.resolve('./read_ml_access')); + loadTestFile(require.resolve('./no_ml_access')); + }); +} diff --git a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts new file mode 100644 index 0000000000000..6fd78458a6ce5 --- /dev/null +++ b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts @@ -0,0 +1,72 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +import { USER } from '../../../services/ml/security_common'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'error']); + const ml = getService('ml'); + + const testUsers = [USER.ML_UNAUTHORIZED, USER.ML_UNAUTHORIZED_SPACES]; + + describe('for user with no ML access', function () { + this.tags(['skipFirefox', 'mlqa']); + + for (const user of testUsers) { + describe(`(${user})`, function () { + before(async () => { + await ml.securityUI.loginAs(user); + }); + + after(async () => { + await ml.securityUI.logout(); + }); + + it('should not allow to access the ML app', async () => { + await ml.testExecution.logTestStep('should not load the ML overview page'); + await PageObjects.common.navigateToUrl('ml', '', { + shouldLoginIfPrompted: false, + ensureCurrentUrl: false, + }); + + await PageObjects.error.expectNotFound(); + }); + + it('should not display the ML file data vis link on the Kibana home page', async () => { + await ml.testExecution.logTestStep('should load the Kibana home page'); + await ml.navigation.navigateToKibanaHome(); + + await ml.testExecution.logTestStep('should not display the ML file data vis link'); + await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); + }); + + it('should not display the ML entry in Kibana app menu', async () => { + await ml.testExecution.logTestStep('should open the Kibana app menu'); + await ml.navigation.openKibanaNav(); + + await ml.testExecution.logTestStep('should not display the ML nav link'); + await ml.navigation.assertKibanaNavMLEntryNotExists(); + }); + + it('should not allow to access the Stack Management ML page', async () => { + await ml.testExecution.logTestStep( + 'should load the stack management with the ML menu item being present' + ); + await ml.navigation.navigateToStackManagement(); + + await ml.testExecution.logTestStep( + 'should display the access denied page in stack management' + ); + await ml.navigation.navigateToStackManagementJobsListPage({ + expectAccessDenied: true, + }); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts new file mode 100644 index 0000000000000..a358e57f792c7 --- /dev/null +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -0,0 +1,426 @@ +/* + * 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 path from 'path'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +import { USER } from '../../../services/ml/security_common'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + const testUsers = [USER.ML_VIEWER, USER.ML_VIEWER_SPACES]; + + describe('for user with read ML access', function () { + this.tags(['skipFirefox', 'mlqa']); + + describe('with no data loaded', function () { + for (const user of testUsers) { + describe(`(${user})`, function () { + before(async () => { + await ml.securityUI.loginAs(user); + await ml.api.cleanMlIndices(); + }); + + after(async () => { + await ml.securityUI.logout(); + }); + + it('should not display the ML file data vis link on the Kibana home page', async () => { + await ml.testExecution.logTestStep('should load the Kibana home page'); + await ml.navigation.navigateToKibanaHome(); + + await ml.testExecution.logTestStep('should not display the ML file data vis link'); + await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); + }); + + it('should display the ML entry in Kibana app menu', async () => { + await ml.testExecution.logTestStep('should open the Kibana app menu'); + await ml.navigation.openKibanaNav(); + + await ml.testExecution.logTestStep('should display the ML nav link'); + await ml.navigation.assertKibanaNavMLEntryExists(); + }); + + it('should display elements on ML Overview page correctly', async () => { + await ml.testExecution.logTestStep('should load the ML overview page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToOverview(); + + await ml.testExecution.logTestStep('should display disabled AD create job button'); + await ml.overviewPage.assertADCreateJobButtonExists(); + await ml.overviewPage.assertADCreateJobButtonEnabled(false); + + await ml.testExecution.logTestStep('should display disabled DFA create job button'); + await ml.overviewPage.assertDFACreateJobButtonExists(); + await ml.overviewPage.assertDFACreateJobButtonEnabled(false); + }); + }); + } + }); + + describe('with no data loaded', function () { + const adJobId = 'fq_single_permission'; + const dfaJobId = 'iph_outlier_permission'; + const calendarId = 'calendar_permission'; + const eventDescription = 'calendar_event_permission'; + const filterId = 'filter_permission'; + const filterItems = ['filter_item_permission']; + + const ecIndexPattern = 'ft_module_sample_ecommerce'; + const ecExpectedTotalCount = 287; + const ecExpectedFieldPanelCount = 2; + + const uploadFilePath = path.join( + __dirname, + '..', + 'data_visualizer', + 'files_to_import', + 'artificial_server_log' + ); + const expectedUploadFileTitle = 'artificial_server_log'; + + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await esArchiver.loadIfNeeded('ml/ihp_outlier'); + await esArchiver.loadIfNeeded('ml/module_sample_ecommerce'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded(ecIndexPattern, 'order_date'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.api.createAndRunAnomalyDetectionLookbackJob( + ml.commonConfig.getADFqMultiMetricJobConfig(adJobId), + ml.commonConfig.getADFqDatafeedConfig(adJobId) + ); + + await ml.api.createAndRunDFAJob( + ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(dfaJobId) + ); + + await ml.api.createCalendar(calendarId, { + calendar_id: calendarId, + job_ids: [], + description: 'Test calendar', + }); + await ml.api.createCalendarEvents(calendarId, [ + { + description: eventDescription, + start_time: 1513641600000, + end_time: 1513728000000, + }, + ]); + + await ml.api.createFilter(filterId, { + description: 'Test filter list', + items: filterItems, + }); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.api.deleteIndices(`user-${dfaJobId}`); + await ml.api.deleteCalendar(calendarId); + await ml.api.deleteFilter(filterId); + }); + + for (const user of testUsers) { + describe(`(${user})`, function () { + before(async () => { + await ml.securityUI.loginAs(user); + }); + + after(async () => { + await ml.securityUI.logout(); + }); + + it('should display elements on Anomaly Detection page correctly', async () => { + await ml.testExecution.logTestStep('should load the AD job management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToAnomalyDetection(); + + await ml.testExecution.logTestStep('should display the stats bar and the AD job table'); + await ml.jobManagement.assertJobStatsBarExists(); + await ml.jobManagement.assertJobTableExists(); + + await ml.testExecution.logTestStep('should display a disabled "Create job" button'); + await ml.jobManagement.assertCreateNewJobButtonExists(); + await ml.jobManagement.assertCreateNewJobButtonEnabled(false); + + await ml.testExecution.logTestStep('should display the AD job in the list'); + await ml.jobTable.filterWithSearchString(adJobId, 1); + + await ml.testExecution.logTestStep('should display enabled AD job result links'); + await ml.jobTable.assertJobActionSingleMetricViewerButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionAnomalyExplorerButtonEnabled(adJobId, true); + + await ml.testExecution.logTestStep('should display disabled AD job row action button'); + await ml.jobTable.assertJobActionsMenuButtonEnabled(adJobId, false); + + await ml.testExecution.logTestStep('should select the job'); + await ml.jobTable.selectJobRow(adJobId); + + await ml.testExecution.logTestStep('should display enabled multi select result links'); + await ml.jobTable.assertMultiSelectActionSingleMetricViewerButtonEnabled(true); + await ml.jobTable.assertMultiSelectActionAnomalyExplorerButtonEnabled(true); + + await ml.testExecution.logTestStep( + 'should display disabled multi select action button' + ); + await ml.jobTable.assertMultiSelectManagementActionsButtonEnabled(false); + await ml.jobTable.deselectJobRow(adJobId); + }); + + it('should display elements on Single Metric Viewer page correctly', async () => { + await ml.testExecution.logTestStep('should open AD job in the single metric viewer'); + await ml.jobTable.clickOpenJobInSingleMetricViewerButton(adJobId); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + + await ml.testExecution.logTestStep('should pre-fill the AD job selection'); + await ml.jobSelection.assertJobSelection([adJobId]); + + await ml.testExecution.logTestStep('should pre-fill the detector input'); + await ml.singleMetricViewer.assertDetectorInputExsist(); + await ml.singleMetricViewer.assertDetectorInputValue('0'); + + await ml.testExecution.logTestStep('should input the airline entity value'); + await ml.singleMetricViewer.assertEntityInputExsist('airline'); + await ml.singleMetricViewer.assertEntityInputSelection('airline', []); + await ml.singleMetricViewer.selectEntityValue('airline', 'AAL'); + + await ml.testExecution.logTestStep('should display the chart'); + await ml.singleMetricViewer.assertChartExsist(); + + await ml.testExecution.logTestStep('should display the annotations section'); + await ml.singleMetricViewer.assertAnnotationsExists('loaded'); + + await ml.testExecution.logTestStep('should display the anomalies table with entries'); + await ml.anomaliesTable.assertTableExists(); + await ml.anomaliesTable.assertTableNotEmpty(); + + await ml.testExecution.logTestStep('should not display the anomaly row action button'); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonNotExists(0); + + await ml.testExecution.logTestStep( + 'should display the forecast modal with disabled run button' + ); + await ml.singleMetricViewer.assertForecastButtonExists(); + await ml.singleMetricViewer.assertForecastButtonEnabled(true); + await ml.singleMetricViewer.openForecastModal(); + await ml.singleMetricViewer.assertForecastModalRunButtonEnabled(false); + await ml.singleMetricViewer.closeForecastModal(); + }); + + it('should display elements on Anomaly Explorer page correctly', async () => { + await ml.testExecution.logTestStep('should open AD job in the anomaly explorer'); + await ml.singleMetricViewer.openAnomalyExplorer(); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + + await ml.testExecution.logTestStep('should pre-fill the AD job selection'); + await ml.jobSelection.assertJobSelection([adJobId]); + + await ml.testExecution.logTestStep('should display the influencers list'); + await ml.anomalyExplorer.assertInfluencerListExists(); + await ml.anomalyExplorer.assertInfluencerFieldListNotEmpty('airline'); + + await ml.testExecution.logTestStep('should display the swim lanes'); + await ml.anomalyExplorer.assertOverallSwimlaneExists(); + await ml.anomalyExplorer.assertSwimlaneViewByExists(); + + await ml.testExecution.logTestStep('should display the annotations panel'); + await ml.anomalyExplorer.assertAnnotationsPanelExists('loaded'); + + await ml.testExecution.logTestStep('should display the anomalies table with entries'); + await ml.anomaliesTable.assertTableExists(); + await ml.anomaliesTable.assertTableNotEmpty(); + + await ml.testExecution.logTestStep('should display enabled anomaly row action button'); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonExists(0); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonEnabled(0, true); + + await ml.testExecution.logTestStep('should not display configure rules action button'); + await ml.anomaliesTable.assertAnomalyActionConfigureRulesButtonNotExists(0); + + await ml.testExecution.logTestStep('should display enabled view series action button'); + await ml.anomaliesTable.assertAnomalyActionViewSeriesButtonEnabled(0, true); + }); + + it('should display elements on Data Frame Analytics page correctly', async () => { + await ml.testExecution.logTestStep('should load the DFA job management page'); + await ml.navigation.navigateToDataFrameAnalytics(); + + await ml.testExecution.logTestStep( + 'should display the stats bar and the analytics table' + ); + await ml.dataFrameAnalytics.assertAnalyticsStatsBarExists(); + await ml.dataFrameAnalytics.assertAnalyticsTableExists(); + + await ml.testExecution.logTestStep('should display a disabled "Create job" button'); + await ml.dataFrameAnalytics.assertCreateNewAnalyticsButtonExists(); + await ml.dataFrameAnalytics.assertCreateNewAnalyticsButtonEnabled(false); + + await ml.testExecution.logTestStep('should display the DFA job in the list'); + await ml.dataFrameAnalyticsTable.filterWithSearchString(dfaJobId, 1); + + await ml.testExecution.logTestStep( + 'should display enabled DFA job view and action menu' + ); + await ml.dataFrameAnalyticsTable.assertJobRowViewButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJowRowActionsMenuButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJobActionViewButtonEnabled(dfaJobId, true); + + await ml.testExecution.logTestStep( + 'should display disabled DFA job row action buttons' + ); + await ml.dataFrameAnalyticsTable.assertJobActionStartButtonEnabled(dfaJobId, false); // job already completed + await ml.dataFrameAnalyticsTable.assertJobActionEditButtonEnabled(dfaJobId, false); + await ml.dataFrameAnalyticsTable.assertJobActionCloneButtonEnabled(dfaJobId, false); + await ml.dataFrameAnalyticsTable.assertJobActionDeleteButtonEnabled(dfaJobId, false); + await ml.dataFrameAnalyticsTable.ensureJobActionsMenuClosed(dfaJobId); + }); + + it('should display elements on Data Frame Analytics results view page correctly', async () => { + await ml.testExecution.logTestStep('displays the results view for created job'); + await ml.dataFrameAnalyticsTable.openResultsView(dfaJobId); + await ml.dataFrameAnalyticsResults.assertOutlierTablePanelExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); + }); + + it('should display elements on Data Visualizer home page correctly', async () => { + await ml.testExecution.logTestStep('should load the data visualizer page'); + await ml.navigation.navigateToDataVisualizer(); + + await ml.testExecution.logTestStep( + 'should display the "import data" card with enabled button' + ); + await ml.dataVisualizer.assertDataVisualizerImportDataCardExists(); + await ml.dataVisualizer.assertUploadFileButtonEnabled(true); + + await ml.testExecution.logTestStep( + 'should display the "select index pattern" card with enabled button' + ); + await ml.dataVisualizer.assertDataVisualizerIndexDataCardExists(); + await ml.dataVisualizer.assertSelectIndexButtonEnabled(true); + }); + + it('should display elements on Index Data Visualizer page correctly', async () => { + await ml.testExecution.logTestStep( + 'should load an index into the data visualizer page' + ); + await ml.dataVisualizer.navigateToIndexPatternSelection(); + await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer(ecIndexPattern); + + await ml.testExecution.logTestStep('should display the time range step'); + await ml.dataVisualizerIndexBased.assertTimeRangeSelectorSectionExists(); + + await ml.testExecution.logTestStep('should load data for full time range'); + await ml.dataVisualizerIndexBased.clickUseFullDataButton(ecExpectedTotalCount); + + await ml.testExecution.logTestStep('should display the panels of fields'); + await ml.dataVisualizerIndexBased.assertFieldsPanelsExist(ecExpectedFieldPanelCount); + + await ml.testExecution.logTestStep('should not display the actions panel'); + await ml.dataVisualizerIndexBased.assertActionsPanelNotExists(); + }); + + it('should display elements on File Data Visualizer page correctly', async () => { + await ml.testExecution.logTestStep( + 'should load the file data visualizer file selection' + ); + await ml.navigation.navigateToDataVisualizer(); + await ml.dataVisualizer.navigateToFileUpload(); + + await ml.testExecution.logTestStep( + 'should select a file and load visualizer result page' + ); + await ml.dataVisualizerFileBased.selectFile(uploadFilePath); + + await ml.testExecution.logTestStep( + 'should displays components of the file details page' + ); + await ml.dataVisualizerFileBased.assertFileTitle(expectedUploadFileTitle); + await ml.dataVisualizerFileBased.assertFileContentPanelExists(); + await ml.dataVisualizerFileBased.assertSummaryPanelExists(); + await ml.dataVisualizerFileBased.assertFileStatsPanelExists(); + await ml.dataVisualizerFileBased.assertImportButtonEnabled(false); + }); + + it('should display elements on Settings home page correctly', async () => { + await ml.testExecution.logTestStep('should load the settings page'); + await ml.navigation.navigateToSettings(); + + await ml.testExecution.logTestStep( + 'should display enabled calendar management and disabled calendar create links' + ); + await ml.settings.assertManageCalendarsLinkExists(); + await ml.settings.assertManageCalendarsLinkEnabled(true); + await ml.settings.assertCreateCalendarLinkExists(); + await ml.settings.assertCreateCalendarLinkEnabled(false); + + await ml.testExecution.logTestStep('should display disabled filter list controls'); + await ml.settings.assertManageFilterListsLinkExists(); + await ml.settings.assertManageFilterListsLinkEnabled(false); + await ml.settings.assertCreateFilterListLinkExists(); + await ml.settings.assertCreateFilterListLinkEnabled(false); + }); + + it('should display elements on Calendar management page correctly', async () => { + await ml.testExecution.logTestStep('should load the calendar management page'); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('should display disabled create calendar button'); + await ml.settingsCalendar.assertCreateCalendarButtonEnabled(false); + + await ml.testExecution.logTestStep('should display the calendar in the list'); + await ml.settingsCalendar.filterWithSearchString(calendarId, 1); + + await ml.testExecution.logTestStep( + 'should not enable delete calendar button on selection' + ); + await ml.settingsCalendar.assertDeleteCalendarButtonEnabled(false); + await ml.settingsCalendar.selectCalendarRow(calendarId); + await ml.settingsCalendar.assertDeleteCalendarButtonEnabled(false); + + await ml.testExecution.logTestStep('should load the calendar edit page'); + await ml.settingsCalendar.openCalendarEditForm(calendarId); + + await ml.testExecution.logTestStep( + 'should display disabled elements of the edit calendar page' + ); + await ml.settingsCalendar.assertApplyToAllJobsSwitchEnabled(false); + await ml.settingsCalendar.assertJobSelectionEnabled(false); + await ml.settingsCalendar.assertJobGroupSelectionEnabled(false); + await ml.settingsCalendar.assertNewEventButtonEnabled(false); + await ml.settingsCalendar.assertImportEventsButtonEnabled(false); + + await ml.testExecution.logTestStep('should display the event in the list'); + await ml.settingsCalendar.assertEventRowExists(eventDescription); + + await ml.testExecution.logTestStep('should display enabled delete event button'); + await ml.settingsCalendar.assertDeleteEventButtonEnabled(eventDescription, false); + }); + + it('should display elements on Stack Management ML page correctly', async () => { + await ml.testExecution.logTestStep( + 'should load the stack management with the ML menu item being present' + ); + await ml.navigation.navigateToStackManagement(); + + await ml.testExecution.logTestStep( + 'should display the access denied page in stack management' + ); + await ml.navigation.navigateToStackManagementJobsListPage({ + expectAccessDenied: true, + }); + }); + }); + } + }); + }); +} diff --git a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js index d6ed6ce13391e..b4eaff7f3ddcf 100644 --- a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js +++ b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js @@ -11,6 +11,7 @@ import mockRolledUpData, { mockIndices } from './hybrid_index_helper'; export default function ({ getService, getPageObjects }) { const es = getService('legacyEs'); const esArchiver = getService('esArchiver'); + const find = getService('find'); const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'settings']); @@ -88,6 +89,15 @@ export default function ({ getService, getPageObjects }) { (i) => i.includes(rollupIndexPatternName) && i.includes('Rollup') ); expect(filteredIndexPatternNames.length).to.be(1); + + // make sure there are no toasts which might be showing unexpected errors + const toastShown = await find.existsByCssSelector('.euiToast'); + expect(toastShown).to.be(false); + + // ensure all fields are available + await PageObjects.settings.clickIndexPatternByName(rollupIndexPatternName); + const fields = await PageObjects.settings.getFieldNames(); + expect(fields).to.eql(['_source', '_id', '_type', '_index', '_score', '@timestamp']); }); after(async () => { diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index d5c972cb8bd1f..b6ccd68bb2096 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../ftr_provider_context'; import { TransformPivotConfig } from '../../../../plugins/transform/public/app/common'; @@ -99,9 +98,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display the original transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(transformConfig.id); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter((row) => row.id === transformConfig.id)).to.have.length(1); + await transform.table.filterWithSearchString(transformConfig.id, 1); await transform.testExecution.logTestStep('should show the actions popover'); await transform.table.assertTransformRowActions(false); @@ -212,9 +209,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display the created transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(testData.transformId); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter((row) => row.id === testData.transformId)).to.have.length(1); + await transform.table.filterWithSearchString(testData.transformId, 1); }); }); } diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index daecc26186ac1..4e2b832838b7d 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../ftr_provider_context'; @@ -402,9 +401,7 @@ export default function ({ getService }: FtrProviderContext) { 'displays the created transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(testData.transformId); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter((row) => row.id === testData.transformId)).to.have.length(1); + await transform.table.filterWithSearchString(testData.transformId, 1); await transform.testExecution.logTestStep( 'transform creation displays details for the created transform in the transform list' diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index d3cbc1159a9c7..229ff97782362 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../ftr_provider_context'; @@ -208,9 +207,7 @@ export default function ({ getService }: FtrProviderContext) { 'displays the created transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(testData.transformId); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter((row) => row.id === testData.transformId)).to.have.length(1); + await transform.table.filterWithSearchString(testData.transformId, 1); await transform.testExecution.logTestStep( 'transform creation displays details for the created transform in the transform list' diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 5582d279833e7..460e7c5b24a98 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -3,7 +3,6 @@ * 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 { FtrProviderContext } from '../../ftr_provider_context'; import { TransformPivotConfig } from '../../../../plugins/transform/public/app/common'; @@ -73,9 +72,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display the original transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(transformConfig.id); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter((row) => row.id === transformConfig.id)).to.have.length(1); + await transform.table.filterWithSearchString(transformConfig.id, 1); await transform.testExecution.logTestStep('should show the actions popover'); await transform.table.assertTransformRowActions(false); @@ -127,9 +124,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display the updated transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(transformConfig.id); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter((row) => row.id === transformConfig.id)).to.have.length(1); + await transform.table.filterWithSearchString(transformConfig.id, 1); await transform.testExecution.logTestStep( 'should display the updated transform in the transform list row cells' diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 6dd22a1f4a204..a01f3fa5d53a5 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -5,10 +5,9 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, loadTestFile, getPageObjects }: FtrProviderContext) { +export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); - const PageObjects = getPageObjects(['security']); describe('transform', function () { this.tags(['ciGroup9', 'transform']); @@ -31,7 +30,7 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid await esArchiver.unload('ml/ecommerce'); await transform.testResources.resetKibanaTimeZone(); - await PageObjects.security.logout(); + await transform.securityUI.logout(); }); loadTestFile(require.resolve('./creation_index_pattern')); diff --git a/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz b/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz new file mode 100644 index 0000000000000..e1b9c01101f6e Binary files /dev/null and b/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/endpoint/resolver/signals/mappings.json b/x-pack/test/functional/es_archives/endpoint/resolver/signals/mappings.json new file mode 100644 index 0000000000000..ad77961a41445 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/resolver/signals/mappings.json @@ -0,0 +1,3239 @@ +{ + "type": "index", + "value": { + "aliases": { + ".siem-signals-default": { + "is_write_index": true + } + }, + "index": ".siem-signals-default-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "doc_values": false, + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "signal": { + "properties": { + "ancestors": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_event": { + "properties": { + "action": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + }, + "module": { + "type": "keyword" + }, + "original": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "outcome": { + "type": "keyword" + }, + "provider": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_time": { + "type": "date" + }, + "parent": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "type": "keyword" + }, + "building_block_type": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "false_positives": { + "type": "keyword" + }, + "filters": { + "type": "object" + }, + "from": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "immutable": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "language": { + "type": "keyword" + }, + "license": { + "type": "keyword" + }, + "max_signals": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "output_index": { + "type": "keyword" + }, + "query": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "rule_id": { + "type": "keyword" + }, + "rule_name_override": { + "type": "keyword" + }, + "saved_id": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "severity_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "size": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + } + } + }, + "threshold": { + "properties": { + "field": { + "type": "keyword" + }, + "value": { + "type": "float" + } + } + }, + "timeline_id": { + "type": "keyword" + }, + "timeline_title": { + "type": "keyword" + }, + "timestamp_override": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "threshold_count": { + "type": "float" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": ".siem-signals-default", + "rollover_alias": ".siem-signals-default" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "1", + "routing": { + "allocation": { + "include": { + "_tier": "data_hot" + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/services/dashboard/drilldowns_manage.ts b/x-pack/test/functional/services/dashboard/drilldowns_manage.ts index a01fde3a5233f..7b66591fcf764 100644 --- a/x-pack/test/functional/services/dashboard/drilldowns_manage.ts +++ b/x-pack/test/functional/services/dashboard/drilldowns_manage.ts @@ -12,6 +12,8 @@ const DASHBOARD_TO_DASHBOARD_ACTION_LIST_ITEM = 'actionFactoryItem-DASHBOARD_TO_DASHBOARD_DRILLDOWN'; const DASHBOARD_TO_DASHBOARD_ACTION_WIZARD = 'selectedActionFactory-DASHBOARD_TO_DASHBOARD_DRILLDOWN'; +const DASHBOARD_TO_URL_ACTION_LIST_ITEM = 'actionFactoryItem-URL_DRILLDOWN'; +const DASHBOARD_TO_URL_ACTION_WIZARD = 'selectedActionFactory-URL_DRILLDOWN'; const DESTINATION_DASHBOARD_SELECT = 'dashboardDrilldownSelectDashboard'; const DRILLDOWN_WIZARD_SUBMIT = 'drilldownWizardSubmit'; @@ -68,10 +70,32 @@ export function DashboardDrilldownsManageProvider({ getService }: FtrProviderCon await this.selectDestinationDashboard(destinationDashboardTitle); } + async fillInDashboardToURLDrilldownWizard({ + drilldownName, + destinationURLTemplate, + trigger, + }: { + drilldownName: string; + destinationURLTemplate: string; + trigger: 'VALUE_CLICK_TRIGGER' | 'SELECT_RANGE_TRIGGER'; + }) { + await this.fillInDrilldownName(drilldownName); + await this.selectDashboardToURLActionIfNeeded(); + await this.selectTriggerIfNeeded(trigger); + await this.fillInURLTemplate(destinationURLTemplate); + } + async fillInDrilldownName(name: string) { await testSubjects.setValue('drilldownNameInput', name); } + async selectDashboardToURLActionIfNeeded() { + if (await testSubjects.exists(DASHBOARD_TO_URL_ACTION_LIST_ITEM)) { + await testSubjects.click(DASHBOARD_TO_URL_ACTION_LIST_ITEM); + } + await testSubjects.existOrFail(DASHBOARD_TO_URL_ACTION_WIZARD); + } + async selectDashboardToDashboardActionIfNeeded() { if (await testSubjects.exists(DASHBOARD_TO_DASHBOARD_ACTION_LIST_ITEM)) { await testSubjects.click(DASHBOARD_TO_DASHBOARD_ACTION_LIST_ITEM); @@ -83,6 +107,18 @@ export function DashboardDrilldownsManageProvider({ getService }: FtrProviderCon await comboBox.set(DESTINATION_DASHBOARD_SELECT, title); } + async selectTriggerIfNeeded(trigger: 'VALUE_CLICK_TRIGGER' | 'SELECT_RANGE_TRIGGER') { + if (await testSubjects.exists(`triggerPicker`)) { + const container = await testSubjects.find(`triggerPicker-${trigger}`); + const radio = await container.findByCssSelector('input[type=radio]'); + await radio.click(); + } + } + + async fillInURLTemplate(destinationURLTemplate: string) { + await testSubjects.setValue('urlInput', destinationURLTemplate); + } + async saveChanges() { await testSubjects.click(DRILLDOWN_WIZARD_SUBMIT); } diff --git a/x-pack/test/functional/services/ml/anomalies_table.ts b/x-pack/test/functional/services/ml/anomalies_table.ts index 26af97d008feb..0231d941fb85f 100644 --- a/x-pack/test/functional/services/ml/anomalies_table.ts +++ b/x-pack/test/functional/services/ml/anomalies_table.ts @@ -3,11 +3,13 @@ * 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 { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningAnomaliesTableProvider({ getService }: FtrProviderContext) { + const retry = getService('retry'); const testSubjects = getService('testSubjects'); return { @@ -15,12 +17,97 @@ export function MachineLearningAnomaliesTableProvider({ getService }: FtrProvide await testSubjects.existOrFail('mlAnomaliesTable'); }, + async getTableRows() { + return await testSubjects.findAll('mlAnomaliesTable > ~mlAnomaliesListRow'); + }, + + async getRowSubjByRowIndex(rowIndex: number) { + const tableRows = await this.getTableRows(); + expect(tableRows.length).to.be.greaterThan( + rowIndex, + `Expected anomalies table to have at least ${rowIndex + 1} rows (got ${ + tableRows.length + } rows)` + ); + const row = tableRows[rowIndex]; + const rowSubj = await row.getAttribute('data-test-subj'); + + return rowSubj; + }, + async assertTableNotEmpty() { - const tableRows = await testSubjects.findAll('mlAnomaliesTable > ~mlAnomaliesListRow'); + const tableRows = await this.getTableRows(); expect(tableRows.length).to.be.greaterThan( 0, `Anomalies table should have at least one row (got '${tableRows.length}')` ); }, + + async assertAnomalyActionsMenuButtonExists(rowIndex: number) { + const rowSubj = await this.getRowSubjByRowIndex(rowIndex); + await testSubjects.existOrFail(`${rowSubj} > mlAnomaliesListRowActionsButton`); + }, + + async assertAnomalyActionsMenuButtonNotExists(rowIndex: number) { + const rowSubj = await this.getRowSubjByRowIndex(rowIndex); + await testSubjects.missingOrFail(`${rowSubj} > mlAnomaliesListRowActionsButton`); + }, + + async assertAnomalyActionsMenuButtonEnabled(rowIndex: number, expectedValue: boolean) { + const rowSubj = await this.getRowSubjByRowIndex(rowIndex); + const isEnabled = await testSubjects.isEnabled( + `${rowSubj} > mlAnomaliesListRowActionsButton` + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected actions menu button for anomalies list entry #${rowIndex} to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async ensureAnomalyActionsMenuOpen(rowIndex: number) { + await retry.tryForTime(30 * 1000, async () => { + const rowSubj = await this.getRowSubjByRowIndex(rowIndex); + if (!(await testSubjects.exists('mlAnomaliesListRowActionsMenu'))) { + await testSubjects.click(`${rowSubj} > mlAnomaliesListRowActionsButton`); + await testSubjects.existOrFail('mlAnomaliesListRowActionsMenu', { timeout: 5000 }); + } + }); + }, + + async assertAnomalyActionConfigureRulesButtonExists(rowIndex: number) { + await this.ensureAnomalyActionsMenuOpen(rowIndex); + await testSubjects.existOrFail('mlAnomaliesListRowActionConfigureRulesButton'); + }, + + async assertAnomalyActionConfigureRulesButtonNotExists(rowIndex: number) { + await this.ensureAnomalyActionsMenuOpen(rowIndex); + await testSubjects.missingOrFail('mlAnomaliesListRowActionConfigureRulesButton'); + }, + + async assertAnomalyActionConfigureRulesButtonEnabled(rowIndex: number, expectedValue: boolean) { + await this.ensureAnomalyActionsMenuOpen(rowIndex); + const isEnabled = await testSubjects.isEnabled( + 'mlAnomaliesListRowActionConfigureRulesButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "configure rules" action button for anomalies list entry #${rowIndex} to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertAnomalyActionViewSeriesButtonEnabled(rowIndex: number, expectedValue: boolean) { + await this.ensureAnomalyActionsMenuOpen(rowIndex); + const isEnabled = await testSubjects.isEnabled('mlAnomaliesListRowActionViewSeriesButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "view series" action button for anomalies list entry #${rowIndex} to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, }; } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 401a96c5c11bd..5c9718539f47b 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -11,7 +11,7 @@ import { Annotation } from '../../../../plugins/ml/common/types/annotations'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; import { FtrProviderContext } from '../../ftr_provider_context'; import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states'; -import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/data_frame_task_state'; import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; export type MlApi = ProvidedType; import { @@ -722,5 +722,25 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { } }); }, + + async runDFAJob(dfaId: string) { + log.debug(`Starting data frame analytics job '${dfaId}'...`); + const startResponse = await esSupertest + .post(`/_ml/data_frame/analytics/${dfaId}/_start`) + .set({ 'Content-Type': 'application/json' }) + .expect(200) + .then((res: any) => res.body); + + expect(startResponse) + .to.have.property('acknowledged') + .eql(true, 'Response for start data frame analytics job request should be acknowledged'); + }, + + async createAndRunDFAJob(dfaConfig: DataFrameAnalyticsConfig) { + await this.createDataFrameAnalyticsJob(dfaConfig); + await this.runDFAJob(dfaConfig.id); + await this.waitForDFAJobTrainingRecordCountToBePositive(dfaConfig.id); + await this.waitForAnalyticsState(dfaConfig.id, DATA_FRAME_TASK_STATE.STOPPED); + }, }; } diff --git a/x-pack/test/functional/services/ml/common_config.ts b/x-pack/test/functional/services/ml/common_config.ts new file mode 100644 index 0000000000000..74538145135bc --- /dev/null +++ b/x-pack/test/functional/services/ml/common_config.ts @@ -0,0 +1,112 @@ +/* + * 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 { DeepPartial } from '../../../../plugins/ml/common/types/common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { Job, Datafeed } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; +import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; + +const FQ_SM_JOB_CONFIG: Job = { + job_id: ``, + description: 'mean(responsetime) on farequote dataset with 15m bucket span', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + model_plot_config: { enabled: true }, +}; + +const FQ_MM_JOB_CONFIG: Job = { + job_id: `fq_multi_1_ae`, + description: + 'mean/min/max(responsetime) partition=airline on farequote dataset with 1h bucket span', + groups: ['farequote', 'automated', 'multi-metric'], + analysis_config: { + bucket_span: '1h', + influencers: ['airline'], + detectors: [ + { function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }, + { function: 'min', field_name: 'responsetime', partition_field_name: 'airline' }, + { function: 'max', field_name: 'responsetime', partition_field_name: 'airline' }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '20mb' }, + model_plot_config: { enabled: true }, +}; + +const FQ_DATAFEED_CONFIG: Datafeed = { + datafeed_id: '', + indices: ['ft_farequote'], + job_id: '', + query: { bool: { must: [{ match_all: {} }] } }, +}; + +const IHP_OUTLIER_DETECTION_CONFIG: DeepPartial = { + id: '', + description: 'Outlier detection job based on the Iowa house prices dataset', + source: { + index: ['ft_ihp_outlier'], + query: { + match_all: {}, + }, + }, + dest: { + index: '', + results_field: 'ml', + }, + analysis: { + outlier_detection: {}, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '5mb', +}; + +export function MachineLearningCommonConfigsProvider({}: FtrProviderContext) { + return { + getADFqSingleMetricJobConfig(jobId: string): Job { + const jobConfig = { ...FQ_SM_JOB_CONFIG, job_id: jobId }; + return jobConfig; + }, + + getADFqMultiMetricJobConfig(jobId: string): Job { + const jobConfig = { ...FQ_MM_JOB_CONFIG, job_id: jobId }; + return jobConfig; + }, + + getADFqDatafeedConfig(jobId: string): Datafeed { + const datafeedConfig = { + ...FQ_DATAFEED_CONFIG, + datafeed_id: `datafeed-${jobId}`, + job_id: jobId, + }; + return datafeedConfig; + }, + + getDFAIhpOutlierDetectionJobConfig(dfaId: string): DataFrameAnalyticsConfig { + const dfaConfig = { + ...IHP_OUTLIER_DETECTION_CONFIG, + id: dfaId, + dest: { ...IHP_OUTLIER_DETECTION_CONFIG.dest, index: `user-${dfaId}` }, + }; + return dfaConfig as DataFrameAnalyticsConfig; + }, + }; +} diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index b66fd7087654d..319dc54fa6421 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -78,5 +78,13 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte await testSubjects.missingOrFail('mlLoadingIndicator'); }); }, + + async assertKibanaHomeFileDataVisLinkExists() { + await testSubjects.existOrFail('homeSynopsisLinkml_file_data_visualizer'); + }, + + async assertKibanaHomeFileDataVisLinkNotExists() { + await testSubjects.missingOrFail('homeSynopsisLinkml_file_data_visualizer'); + }, }; } diff --git a/x-pack/test/functional/services/ml/data_frame_analytics.ts b/x-pack/test/functional/services/ml/data_frame_analytics.ts index 634b0d4d41fca..670e16ce4af94 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; import { MlApi } from './api'; @@ -32,29 +34,14 @@ export function MachineLearningDataFrameAnalyticsProvider( await testSubjects.existOrFail('mlAnalyticsButtonCreate'); }, - async assertRegressionEvaluatePanelElementsExists() { - await testSubjects.existOrFail('mlDFAnalyticsRegressionExplorationEvaluatePanel'); - await testSubjects.existOrFail('mlDFAnalyticsRegressionGenMSEstat'); - await testSubjects.existOrFail('mlDFAnalyticsRegressionGenRSquaredStat'); - await testSubjects.existOrFail('mlDFAnalyticsRegressionTrainingMSEstat'); - await testSubjects.existOrFail('mlDFAnalyticsRegressionTrainingRSquaredStat'); - }, - - async assertRegressionTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); - }, - - async assertClassificationEvaluatePanelElementsExists() { - await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationEvaluatePanel'); - await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationConfusionMatrix'); - }, - - async assertClassificationTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); - }, - - async assertOutlierTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsOutlierExplorationTablePanel'); + async assertCreateNewAnalyticsButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlAnalyticsButtonCreate'); + expect(isEnabled).to.eql( + expectedValue, + `Expected data frame analytics "create" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); }, async assertAnalyticsStatsBarExists() { diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts new file mode 100644 index 0000000000000..b6a6ff8eb6c63 --- /dev/null +++ b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts @@ -0,0 +1,58 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningDataFrameAnalyticsResultsProvider({ + getService, +}: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async assertRegressionEvaluatePanelElementsExists() { + await testSubjects.existOrFail('mlDFAnalyticsRegressionExplorationEvaluatePanel'); + await testSubjects.existOrFail('mlDFAnalyticsRegressionGenMSEstat'); + await testSubjects.existOrFail('mlDFAnalyticsRegressionGenRSquaredStat'); + await testSubjects.existOrFail('mlDFAnalyticsRegressionTrainingMSEstat'); + await testSubjects.existOrFail('mlDFAnalyticsRegressionTrainingRSquaredStat'); + }, + + async assertRegressionTablePanelExists() { + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); + }, + + async assertClassificationEvaluatePanelElementsExists() { + await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationEvaluatePanel'); + await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationConfusionMatrix'); + }, + + async assertClassificationTablePanelExists() { + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); + }, + + async assertOutlierTablePanelExists() { + await testSubjects.existOrFail('mlDFAnalyticsOutlierExplorationTablePanel'); + }, + + async assertResultsTableExists() { + await testSubjects.existOrFail('mlExplorationDataGrid loaded', { timeout: 5000 }); + }, + + async getResultTableRows() { + return await testSubjects.findAll('mlExplorationDataGrid loaded > dataGridRow'); + }, + + async assertResultsTableNotEmpty() { + const resultTableRows = await this.getResultTableRows(); + expect(resultTableRows.length).to.be.greaterThan( + 0, + `DFA results table should have at least one row (got '${resultTableRows.length}')` + ); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts index 608a1f2bee3e1..cd2f26f3a660d 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts @@ -9,8 +9,9 @@ import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrap import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); const find = getService('find'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); return new (class AnalyticsTable { public async parseAnalyticsTable() { @@ -62,6 +63,11 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F return rows; } + public rowSelector(analyticsId: string, subSelector?: string) { + const row = `~mlAnalyticsTable > ~row-${analyticsId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + } + public async waitForRefreshButtonLoaded() { await testSubjects.existOrFail('~mlAnalyticsRefreshListButton', { timeout: 10 * 1000 }); await testSubjects.existOrFail('mlAnalyticsRefreshListButton loaded', { timeout: 30 * 1000 }); @@ -84,17 +90,29 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F return await tableListContainer.findByClassName('euiFieldSearch'); } - async assertJobViewButtonExists() { - await testSubjects.existOrFail('mlAnalyticsJobViewButton'); + public async assertJobRowViewButtonExists(analyticsId: string) { + await testSubjects.existOrFail(this.rowSelector(analyticsId, 'mlAnalyticsJobViewButton')); } - public async openEditFlyout(analyticsId: string) { - await this.openRowActions(analyticsId); - await testSubjects.click('mlAnalyticsJobEditButton'); - await testSubjects.existOrFail('mlAnalyticsEditFlyout', { timeout: 5000 }); + public async assertJobRowViewButtonEnabled(analyticsId: string, expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + this.rowSelector(analyticsId, 'mlAnalyticsJobViewButton') + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected data frame analytics row "view results" button for job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async openResultsView(analyticsId: string) { + await this.assertJobRowViewButtonExists(analyticsId); + await testSubjects.click(this.rowSelector(analyticsId, 'mlAnalyticsJobViewButton')); + await testSubjects.existOrFail('mlPageDataFrameAnalyticsExploration', { timeout: 20 * 1000 }); } - async assertAnalyticsSearchInputValue(expectedSearchValue: string) { + public async assertAnalyticsSearchInputValue(expectedSearchValue: string) { const searchBarInput = await this.getAnalyticsSearchInput(); const actualSearchValue = await searchBarInput.getAttribute('value'); expect(actualSearchValue).to.eql( @@ -103,18 +121,19 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F ); } - public async openResultsView() { - await this.assertJobViewButtonExists(); - await testSubjects.click('mlAnalyticsJobViewButton'); - await testSubjects.existOrFail('mlPageDataFrameAnalyticsExploration', { timeout: 20 * 1000 }); - } - - public async filterWithSearchString(filter: string) { + public async filterWithSearchString(filter: string, expectedRowCount: number = 1) { await this.waitForAnalyticsToLoad(); const searchBarInput = await this.getAnalyticsSearchInput(); await searchBarInput.clearValueWithKeyboard(); await searchBarInput.type(filter); await this.assertAnalyticsSearchInputValue(filter); + + const rows = await this.parseAnalyticsTable(); + const filteredRows = rows.filter((row) => row.id === filter); + expect(filteredRows).to.have.length( + expectedRowCount, + `Filtered DFA job table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + ); } public async assertAnalyticsRowFields(analyticsId: string, expectedRow: object) { @@ -129,15 +148,102 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F ); } - public async openRowActions(analyticsId: string) { - await find.clickByCssSelector( - `[data-test-subj="mlAnalyticsTableRow row-${analyticsId}"] [data-test-subj=euiCollapsedItemActionsButton]` + public async assertJowRowActionsMenuButtonEnabled(analyticsId: string, expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + this.rowSelector(analyticsId, 'euiCollapsedItemActionsButton') + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected row action menu button for DFA job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async ensureJobActionsMenuOpen(analyticsId: string) { + await retry.tryForTime(30 * 1000, async () => { + if (!(await testSubjects.exists('mlAnalyticsJobDeleteButton'))) { + await testSubjects.click(this.rowSelector(analyticsId, 'euiCollapsedItemActionsButton')); + await testSubjects.existOrFail('mlAnalyticsJobDeleteButton', { timeout: 5000 }); + } + }); + } + + public async ensureJobActionsMenuClosed(analyticsId: string) { + await retry.tryForTime(30 * 1000, async () => { + if (await testSubjects.exists('mlAnalyticsJobDeleteButton')) { + await testSubjects.click(this.rowSelector(analyticsId, 'euiCollapsedItemActionsButton')); + await testSubjects.missingOrFail('mlAnalyticsJobDeleteButton', { timeout: 5000 }); + } + }); + } + + public async assertJobActionViewButtonEnabled(analyticsId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(analyticsId); + const actionMenuViewButton = await find.byCssSelector( + '[data-test-subj="mlAnalyticsJobViewButton"][class="euiContextMenuItem"]' + ); + const isEnabled = await actionMenuViewButton.isEnabled(); + expect(isEnabled).to.eql( + expectedValue, + `Expected "view" action menu button for DFA job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionStartButtonEnabled(analyticsId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(analyticsId); + const isEnabled = await testSubjects.isEnabled('mlAnalyticsJobStartButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "start" action menu button for DFA job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionEditButtonEnabled(analyticsId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(analyticsId); + const isEnabled = await testSubjects.isEnabled('mlAnalyticsJobEditButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "edit" action menu button for DFA job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - await find.existsByCssSelector('.euiPanel', 20 * 1000); + } + + public async assertJobActionCloneButtonEnabled(analyticsId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(analyticsId); + const isEnabled = await testSubjects.isEnabled('mlAnalyticsJobCloneButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "clone" action menu button for DFA job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionDeleteButtonEnabled(analyticsId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(analyticsId); + const isEnabled = await testSubjects.isEnabled('mlAnalyticsJobDeleteButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "delete" action menu button for DFA job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async openEditFlyout(analyticsId: string) { + await this.ensureJobActionsMenuOpen(analyticsId); + await testSubjects.click('mlAnalyticsJobEditButton'); + await testSubjects.existOrFail('mlAnalyticsEditFlyout', { timeout: 5000 }); } public async cloneJob(analyticsId: string) { - await this.openRowActions(analyticsId); + await this.ensureJobActionsMenuOpen(analyticsId); await testSubjects.click(`mlAnalyticsJobCloneButton`); await testSubjects.existOrFail('mlAnalyticsCreationContainer'); } diff --git a/x-pack/test/functional/services/ml/data_visualizer.ts b/x-pack/test/functional/services/ml/data_visualizer.ts index c60ae29b6b3f4..976410d43a28f 100644 --- a/x-pack/test/functional/services/ml/data_visualizer.ts +++ b/x-pack/test/functional/services/ml/data_visualizer.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningDataVisualizerProvider({ getService }: FtrProviderContext) { @@ -18,6 +20,26 @@ export function MachineLearningDataVisualizerProvider({ getService }: FtrProvide await testSubjects.existOrFail('mlDataVisualizerCardIndexData'); }, + async assertSelectIndexButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlDataVisualizerSelectIndexButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "select index" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertUploadFileButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlDataVisualizerUploadFileButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "upload file" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async navigateToIndexPatternSelection() { await testSubjects.click('mlDataVisualizerSelectIndexButton'); await testSubjects.existOrFail('mlPageSourceSelection'); diff --git a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts index 14c6f8de7d329..61496debb97e2 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts @@ -54,6 +54,16 @@ export function MachineLearningDataVisualizerFileBasedProvider( await testSubjects.existOrFail('mlFileDataVisFileStatsPanel'); }, + async assertImportButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFileDataVisOpenImportPageButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "import" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async navigateToFileImport() { await testSubjects.click('mlFileDataVisOpenImportPageButton'); await testSubjects.existOrFail('mlPageFileDataVisImport'); diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts index 7789ca78363df..31cd17e4df826 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts @@ -119,6 +119,30 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ await this.assertFieldsPanelCardCount(panelFieldTypes, expectedCardCount); }, + async assertActionsPanelExists() { + await testSubjects.existOrFail('mlDataVisualizerActionsPanel'); + }, + + async assertActionsPanelNotExists() { + await testSubjects.missingOrFail('mlDataVisualizerActionsPanel'); + }, + + async assertCreateAdvancedJobCardExists() { + await testSubjects.existOrFail('mlDataVisualizerCreateAdvancedJobCard'); + }, + + async assertCreateAdvancedJobCardNotExists() { + await testSubjects.missingOrFail('mlDataVisualizerCreateAdvancedJobCard'); + }, + + async assertRecognizerCardExists(moduleId: string) { + await testSubjects.existOrFail(`mlRecognizerCard ${moduleId}`); + }, + + async assertRecognizerCardNotExists(moduleId: string) { + await testSubjects.missingOrFail(`mlRecognizerCard ${moduleId}`); + }, + async clickCreateAdvancedJobButton() { await testSubjects.clickWhenNotDisabled('mlDataVisualizerCreateAdvancedJobCard'); }, diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index d7ff60440bf31..325ea41ae3977 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -10,11 +10,13 @@ import { MachineLearningAnomaliesTableProvider } from './anomalies_table'; import { MachineLearningAnomalyExplorerProvider } from './anomaly_explorer'; import { MachineLearningAPIProvider } from './api'; import { MachineLearningCommonAPIProvider } from './common_api'; +import { MachineLearningCommonConfigsProvider } from './common_config'; import { MachineLearningCommonUIProvider } from './common_ui'; import { MachineLearningCustomUrlsProvider } from './custom_urls'; import { MachineLearningDataFrameAnalyticsProvider } from './data_frame_analytics'; import { MachineLearningDataFrameAnalyticsCreationProvider } from './data_frame_analytics_creation'; import { MachineLearningDataFrameAnalyticsEditProvider } from './data_frame_analytics_edit'; +import { MachineLearningDataFrameAnalyticsResultsProvider } from './data_frame_analytics_results'; import { MachineLearningDataFrameAnalyticsTableProvider } from './data_frame_analytics_table'; import { MachineLearningDataVisualizerProvider } from './data_visualizer'; import { MachineLearningDataVisualizerFileBasedProvider } from './data_visualizer_file_based'; @@ -30,9 +32,12 @@ import { MachineLearningJobWizardCategorizationProvider } from './job_wizard_cat import { MachineLearningJobWizardMultiMetricProvider } from './job_wizard_multi_metric'; import { MachineLearningJobWizardPopulationProvider } from './job_wizard_population'; import { MachineLearningNavigationProvider } from './navigation'; +import { MachineLearningOverviewPageProvider } from './overview_page'; import { MachineLearningSecurityCommonProvider } from './security_common'; import { MachineLearningSecurityUIProvider } from './security_ui'; import { MachineLearningSettingsProvider } from './settings'; +import { MachineLearningSettingsCalendarProvider } from './settings_calendar'; +import { MachineLearningSettingsFilterListProvider } from './settings_filter_list'; import { MachineLearningSingleMetricViewerProvider } from './single_metric_viewer'; import { MachineLearningTestExecutionProvider } from './test_execution'; import { MachineLearningTestResourcesProvider } from './test_resources'; @@ -44,6 +49,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const anomaliesTable = MachineLearningAnomaliesTableProvider(context); const anomalyExplorer = MachineLearningAnomalyExplorerProvider(context); const api = MachineLearningAPIProvider(context); + const commonConfig = MachineLearningCommonConfigsProvider(context); const customUrls = MachineLearningCustomUrlsProvider(context); const dataFrameAnalytics = MachineLearningDataFrameAnalyticsProvider(context, api); const dataFrameAnalyticsCreation = MachineLearningDataFrameAnalyticsCreationProvider( @@ -52,6 +58,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { api ); const dataFrameAnalyticsEdit = MachineLearningDataFrameAnalyticsEditProvider(context, commonUI); + const dataFrameAnalyticsResults = MachineLearningDataFrameAnalyticsResultsProvider(context); const dataFrameAnalyticsTable = MachineLearningDataFrameAnalyticsTableProvider(context); const dataVisualizer = MachineLearningDataVisualizerProvider(context); const dataVisualizerFileBased = MachineLearningDataVisualizerFileBasedProvider(context, commonUI); @@ -67,9 +74,12 @@ export function MachineLearningProvider(context: FtrProviderContext) { const jobWizardMultiMetric = MachineLearningJobWizardMultiMetricProvider(context); const jobWizardPopulation = MachineLearningJobWizardPopulationProvider(context); const navigation = MachineLearningNavigationProvider(context); + const overviewPage = MachineLearningOverviewPageProvider(context); const securityCommon = MachineLearningSecurityCommonProvider(context); const securityUI = MachineLearningSecurityUIProvider(context, securityCommon); const settings = MachineLearningSettingsProvider(context); + const settingsCalendar = MachineLearningSettingsCalendarProvider(context); + const settingsFilterList = MachineLearningSettingsFilterListProvider(context); const singleMetricViewer = MachineLearningSingleMetricViewerProvider(context); const testExecution = MachineLearningTestExecutionProvider(context); const testResources = MachineLearningTestResourcesProvider(context); @@ -79,11 +89,13 @@ export function MachineLearningProvider(context: FtrProviderContext) { anomalyExplorer, api, commonAPI, + commonConfig, commonUI, customUrls, dataFrameAnalytics, dataFrameAnalyticsCreation, dataFrameAnalyticsEdit, + dataFrameAnalyticsResults, dataFrameAnalyticsTable, dataVisualizer, dataVisualizerFileBased, @@ -99,9 +111,12 @@ export function MachineLearningProvider(context: FtrProviderContext) { jobWizardMultiMetric, jobWizardPopulation, navigation, + overviewPage, securityCommon, securityUI, settings, + settingsCalendar, + settingsFilterList, singleMetricViewer, testExecution, testResources, diff --git a/x-pack/test/functional/services/ml/job_management.ts b/x-pack/test/functional/services/ml/job_management.ts index 085bb31258012..4c6148ad6fac6 100644 --- a/x-pack/test/functional/services/ml/job_management.ts +++ b/x-pack/test/functional/services/ml/job_management.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; import { MlApi } from './api'; @@ -29,6 +31,16 @@ export function MachineLearningJobManagementProvider( await testSubjects.existOrFail('mlCreateNewJobButton'); }, + async assertCreateNewJobButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCreateNewJobButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD "Create job" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async assertJobStatsBarExists() { await testSubjects.existOrFail('~mlJobStatsBar'); }, diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts index 58a1afad88e11..54c03c876af8a 100644 --- a/x-pack/test/functional/services/ml/job_table.ts +++ b/x-pack/test/functional/services/ml/job_table.ts @@ -158,12 +158,19 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte await testSubjects.existOrFail('mlJobListTable loaded', { timeout: 30 * 1000 }); } - public async filterWithSearchString(filter: string) { + public async filterWithSearchString(filter: string, expectedRowCount: number = 1) { await this.waitForJobsToLoad(); const searchBar = await testSubjects.find('mlJobListSearchBar'); const searchBarInput = await searchBar.findByTagName('input'); await searchBarInput.clearValueWithKeyboard(); await searchBarInput.type(filter); + + const rows = await this.parseJobTable(); + const filteredRows = rows.filter((row) => row.id === filter); + expect(filteredRows).to.have.length( + expectedRowCount, + `Filtered AD job table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + ); } public async assertJobRowFields(jobId: string, expectedRow: object) { @@ -201,7 +208,93 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte } } - public async clickActionsMenu(jobId: string) { + public async assertJobActionSingleMetricViewerButtonEnabled( + jobId: string, + expectedValue: boolean + ) { + const isEnabled = await testSubjects.isEnabled( + this.rowSelector(jobId, 'mlOpenJobsInSingleMetricViewerButton') + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "open in single metric viewer" job action button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionAnomalyExplorerButtonEnabled( + jobId: string, + expectedValue: boolean + ) { + const isEnabled = await testSubjects.isEnabled( + this.rowSelector(jobId, 'mlOpenJobsInAnomalyExplorerButton') + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "open in anomaly explorer" job action button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionsMenuButtonEnabled(jobId: string, expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + this.rowSelector(jobId, 'euiCollapsedItemActionsButton') + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected actions menu button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionStartDatafeedButtonEnabled(jobId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(jobId); + const isEnabled = await testSubjects.isEnabled('mlActionButtonStartDatafeed'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "start datafeed" action button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionCloneJobButtonEnabled(jobId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(jobId); + const isEnabled = await testSubjects.isEnabled('mlActionButtonCloneJob'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "clone job" action button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionEditJobButtonEnabled(jobId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(jobId); + const isEnabled = await testSubjects.isEnabled('mlActionButtonEditJob'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "edit job" action button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionDeleteJobButtonEnabled(jobId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(jobId); + const isEnabled = await testSubjects.isEnabled('mlActionButtonDeleteJob'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "delete job" action button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async ensureJobActionsMenuOpen(jobId: string) { await retry.tryForTime(30 * 1000, async () => { if (!(await testSubjects.exists('mlActionButtonDeleteJob'))) { await testSubjects.click(this.rowSelector(jobId, 'euiCollapsedItemActionsButton')); @@ -211,13 +304,13 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte } public async clickCloneJobAction(jobId: string) { - await this.clickActionsMenu(jobId); + await this.ensureJobActionsMenuOpen(jobId); await testSubjects.click('mlActionButtonCloneJob'); await testSubjects.existOrFail('~mlPageJobWizard'); } public async clickDeleteJobAction(jobId: string) { - await this.clickActionsMenu(jobId); + await this.ensureJobActionsMenuOpen(jobId); await testSubjects.click('mlActionButtonDeleteJob'); await testSubjects.existOrFail('mlDeleteJobConfirmModal'); } @@ -228,13 +321,138 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte } public async clickOpenJobInSingleMetricViewerButton(jobId: string) { - await testSubjects.click(`~openJobsInSingleMetricViewer-${jobId}`); + await testSubjects.click(this.rowSelector(jobId, 'mlOpenJobsInSingleMetricViewerButton')); await testSubjects.existOrFail('~mlPageSingleMetricViewer'); } public async clickOpenJobInAnomalyExplorerButton(jobId: string) { - await testSubjects.click(`~openJobsInSingleAnomalyExplorer-${jobId}`); + await testSubjects.click(this.rowSelector(jobId, 'mlOpenJobsInAnomalyExplorerButton')); await testSubjects.existOrFail('~mlPageAnomalyExplorer'); } + + public async isJobRowSelected(jobId: string): Promise { + return await testSubjects.isChecked(this.rowSelector(jobId, `checkboxSelectRow-${jobId}`)); + } + + public async assertJobRowSelected(jobId: string, expectedValue: boolean) { + const isSelected = await this.isJobRowSelected(jobId); + expect(isSelected).to.eql( + expectedValue, + `Expected job row for AD job '${jobId}' to be '${ + expectedValue ? 'selected' : 'deselected' + }' (got '${isSelected ? 'selected' : 'deselected'}')` + ); + } + + public async selectJobRow(jobId: string) { + if ((await this.isJobRowSelected(jobId)) === false) { + await testSubjects.click(this.rowSelector(jobId, `checkboxSelectRow-${jobId}`)); + } + + await this.assertJobRowSelected(jobId, true); + await this.assertMultiSelectActionsAreaActive(); + } + + public async deselectJobRow(jobId: string) { + if ((await this.isJobRowSelected(jobId)) === true) { + await testSubjects.click(this.rowSelector(jobId, `checkboxSelectRow-${jobId}`)); + } + + await this.assertJobRowSelected(jobId, false); + await this.assertMultiSelectActionsAreaInactive(); + } + + public async assertMultiSelectActionsAreaActive() { + await testSubjects.existOrFail('mlADJobListMultiSelectActionsArea active'); + } + + public async assertMultiSelectActionsAreaInactive() { + await testSubjects.existOrFail('mlADJobListMultiSelectActionsArea inactive', { + allowHidden: true, + }); + } + + public async assertMultiSelectActionSingleMetricViewerButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + '~mlADJobListMultiSelectActionsArea > mlOpenJobsInSingleMetricViewerButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD jobs multi select "open in single metric viewer" action button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertMultiSelectActionAnomalyExplorerButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + '~mlADJobListMultiSelectActionsArea > mlOpenJobsInAnomalyExplorerButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD jobs multi select "open in anomaly explorer" action button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertMultiSelectActionEditJobGroupsButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + '~mlADJobListMultiSelectActionsArea > mlADJobListMultiSelectEditJobGroupsButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD jobs multi select "edit job groups" action button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertMultiSelectManagementActionsButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + '~mlADJobListMultiSelectActionsArea > mlADJobListMultiSelectManagementActionsButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD jobs multi select "management actions" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertMultiSelectStartDatafeedActionButtonEnabled(expectedValue: boolean) { + await this.ensureMultiSelectManagementActionsMenuOpen(); + const isEnabled = await testSubjects.isEnabled( + 'mlADJobListMultiSelectStartDatafeedActionButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD jobs multi select "management actions" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertMultiSelectDeleteJobActionButtonEnabled(expectedValue: boolean) { + await this.ensureMultiSelectManagementActionsMenuOpen(); + const isEnabled = await testSubjects.isEnabled('mlADJobListMultiSelectDeleteJobActionButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD jobs multi select "management actions" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async ensureMultiSelectManagementActionsMenuOpen() { + await retry.tryForTime(30 * 1000, async () => { + if (!(await testSubjects.exists('mlADJobListMultiSelectDeleteJobActionButton'))) { + await testSubjects.click('mlADJobListMultiSelectManagementActionsButton'); + await testSubjects.existOrFail('mlADJobListMultiSelectDeleteJobActionButton', { + timeout: 5000, + }); + } + }); + } })(); } diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index 116c9deb7c2dc..9b53e5ce2f7e7 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -84,14 +84,22 @@ export function MachineLearningNavigationProvider({ await this.navigateToArea('~mlMainTab & ~settings', 'mlPageSettings'); }, - async navigateToStackManagementJobsListPage() { + async navigateToStackManagementJobsListPage({ + expectAccessDenied = false, + }: { + expectAccessDenied?: boolean; + } = {}) { // clicks the jobsListLink and loads the jobs list page await testSubjects.click('jobsListLink'); await retry.tryForTime(60 * 1000, async () => { - // verify that the overall page is present - await testSubjects.existOrFail('mlPageStackManagementJobsList'); - // verify that the default tab with the anomaly detection jobs list got loaded - await testSubjects.existOrFail('ml-jobs-list'); + if (expectAccessDenied === true) { + await testSubjects.existOrFail('mlPageAccessDenied'); + } else { + // verify that the overall page is present + await testSubjects.existOrFail('mlPageStackManagementJobsList'); + // verify that the default tab with the anomaly detection jobs list got loaded + await testSubjects.existOrFail('ml-jobs-list'); + } }); }, @@ -100,7 +108,7 @@ export function MachineLearningNavigationProvider({ await testSubjects.click('mlStackManagementJobsListAnalyticsTab'); await retry.tryForTime(60 * 1000, async () => { // verify that the empty prompt for analytics jobs list got loaded - await testSubjects.existOrFail('mlNoDataFrameAnalyticsFound'); + await testSubjects.existOrFail('mlAnalyticsJobList'); }); }, @@ -121,5 +129,35 @@ export function MachineLearningNavigationProvider({ await testSubjects.existOrFail('mlPageSingleMetricViewer'); }); }, + + async openKibanaNav() { + if (!(await testSubjects.exists('collapsibleNav'))) { + await testSubjects.click('toggleNavButton'); + } + await testSubjects.existOrFail('collapsibleNav'); + }, + + async assertKibanaNavMLEntryExists() { + const navArea = await testSubjects.find('collapsibleNav'); + const mlNavLink = await navArea.findAllByCssSelector('[title="Machine Learning"]'); + if (mlNavLink.length === 0) { + throw new Error(`expected ML link in nav menu to exist`); + } + }, + + async assertKibanaNavMLEntryNotExists() { + const navArea = await testSubjects.find('collapsibleNav'); + const mlNavLink = await navArea.findAllByCssSelector('[title="Machine Learning"]'); + if (mlNavLink.length !== 0) { + throw new Error(`expected ML link in nav menu to not exist`); + } + }, + + async navigateToKibanaHome() { + await retry.tryForTime(60 * 1000, async () => { + await PageObjects.common.navigateToApp('home'); + await testSubjects.existOrFail('homeApp', { timeout: 2000 }); + }); + }, }; } diff --git a/x-pack/test/functional/services/ml/overview_page.ts b/x-pack/test/functional/services/ml/overview_page.ts new file mode 100644 index 0000000000000..93b95a80cae37 --- /dev/null +++ b/x-pack/test/functional/services/ml/overview_page.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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningOverviewPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async assertADCreateJobButtonExists() { + await testSubjects.existOrFail('mlOverviewCreateADJobButton'); + }, + + async assertADCreateJobButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlOverviewCreateADJobButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD "Create job" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertDFACreateJobButtonExists() { + await testSubjects.existOrFail('mlOverviewCreateDFAJobButton'); + }, + + async assertDFACreateJobButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlOverviewCreateDFAJobButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD "Create job" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/security_common.ts b/x-pack/test/functional/services/ml/security_common.ts index cb331c95accb8..67a28a0ab96cc 100644 --- a/x-pack/test/functional/services/ml/security_common.ts +++ b/x-pack/test/functional/services/ml/security_common.ts @@ -10,9 +10,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export type MlSecurityCommon = ProvidedType; export enum USER { - ML_POWERUSER = 'ml_poweruser', - ML_VIEWER = 'ml_viewer', - ML_UNAUTHORIZED = 'ml_unauthorized', + ML_POWERUSER = 'ft_ml_poweruser', + ML_POWERUSER_SPACES = 'ft_ml_poweruser_spaces', + ML_VIEWER = 'ft_ml_viewer', + ML_VIEWER_SPACES = 'ft_ml_viewer_spaces', + ML_UNAUTHORIZED = 'ft_ml_unauthorized', + ML_UNAUTHORIZED_SPACES = 'ft_ml_unauthorized_spaces', } export function MachineLearningSecurityCommonProvider({ getService }: FtrProviderContext) { @@ -20,53 +23,104 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide const roles = [ { - name: 'ml_source', + name: 'ft_ml_source', elasticsearch: { indices: [{ names: ['*'], privileges: ['read', 'view_index_metadata'] }], }, kibana: [], }, { - name: 'ml_dest', + name: 'ft_ml_source_readonly', + elasticsearch: { + indices: [{ names: ['*'], privileges: ['read'] }], + }, + kibana: [], + }, + { + name: 'ft_ml_dest', elasticsearch: { indices: [{ names: ['user-*'], privileges: ['read', 'index', 'manage'] }], }, kibana: [], }, { - name: 'ml_dest_readonly', + name: 'ft_ml_dest_readonly', elasticsearch: { indices: [{ names: ['user-*'], privileges: ['read'] }], }, kibana: [], }, { - name: 'ml_ui_extras', + name: 'ft_ml_ui_extras', elasticsearch: { cluster: ['manage_ingest_pipelines', 'monitor'], }, kibana: [], }, + { + name: 'ft_default_space_ml_all', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], feature: { ml: ['all'] }, spaces: ['default'] }], + }, + { + name: 'ft_default_space_ml_read', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], feature: { ml: ['read'] }, spaces: ['default'] }], + }, + { + name: 'ft_default_space_ml_none', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], feature: { discover: ['read'] }, spaces: ['default'] }], + }, ]; const users = [ { - name: 'ml_poweruser', + name: 'ft_ml_poweruser', full_name: 'ML Poweruser', password: 'mlp001', - roles: ['kibana_admin', 'machine_learning_admin', 'ml_source', 'ml_dest', 'ml_ui_extras'], + roles: [ + 'kibana_admin', + 'machine_learning_admin', + 'ft_ml_source', + 'ft_ml_dest', + 'ft_ml_ui_extras', + ], }, { - name: 'ml_viewer', + name: 'ft_ml_poweruser_spaces', + full_name: 'ML Poweruser', + password: 'mlps001', + roles: ['ft_default_space_ml_all', 'ft_ml_source', 'ft_ml_dest', 'ft_ml_ui_extras'], + }, + { + name: 'ft_ml_viewer', full_name: 'ML Viewer', password: 'mlv001', - roles: ['kibana_admin', 'machine_learning_user', 'ml_source', 'ml_dest_readonly'], + roles: [ + 'kibana_admin', + 'machine_learning_user', + 'ft_ml_source_readonly', + 'ft_ml_dest_readonly', + ], + }, + { + name: 'ft_ml_viewer_spaces', + full_name: 'ML Viewer', + password: 'mlvs001', + roles: ['ft_default_space_ml_read', 'ft_ml_source_readonly', 'ft_ml_dest_readonly'], }, { - name: 'ml_unauthorized', + name: 'ft_ml_unauthorized', full_name: 'ML Unauthorized', password: 'mlu001', - roles: ['kibana_admin', 'ml_source'], + roles: ['kibana_admin', 'ft_ml_source_readonly'], + }, + { + name: 'ft_ml_unauthorized_spaces', + full_name: 'ML Unauthorized', + password: 'mlus001', + roles: ['ft_default_space_ml_none', 'ft_ml_source_readonly'], }, ]; diff --git a/x-pack/test/functional/services/ml/security_ui.ts b/x-pack/test/functional/services/ml/security_ui.ts index 73516ca58dd5d..e09467ff36a34 100644 --- a/x-pack/test/functional/services/ml/security_ui.ts +++ b/x-pack/test/functional/services/ml/security_ui.ts @@ -31,5 +31,9 @@ export function MachineLearningSecurityUIProvider( async loginAsMlViewer() { await this.loginAs(USER.ML_VIEWER); }, + + async logout() { + await PageObjects.security.forceLogout(); + }, }; } diff --git a/x-pack/test/functional/services/ml/settings.ts b/x-pack/test/functional/services/ml/settings.ts index f7428d06899bf..de77358836fea 100644 --- a/x-pack/test/functional/services/ml/settings.ts +++ b/x-pack/test/functional/services/ml/settings.ts @@ -4,26 +4,78 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningSettingsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); return { - async assertSettingsManageCalendarsLinkExists() { + async assertManageCalendarsLinkExists() { await testSubjects.existOrFail('mlCalendarsMngButton'); }, - async assertSettingsCreateCalendarLinkExists() { + async assertManageCalendarsLinkEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarsMngButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "manage calendars" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertCreateCalendarLinkExists() { await testSubjects.existOrFail('mlCalendarsCreateButton'); }, - async assertSettingsManageFilterListsLinkExists() { + async assertCreateCalendarLinkEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarsCreateButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "create calendars" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertManageFilterListsLinkExists() { await testSubjects.existOrFail('mlFilterListsMngButton'); }, - async assertSettingsCreateFilterListLinkExists() { + async assertManageFilterListsLinkEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListsMngButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "manage filter lists" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertCreateFilterListLinkExists() { await testSubjects.existOrFail('mlFilterListsCreateButton'); }, + + async assertCreateFilterListLinkEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListsCreateButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "create filter lists" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async navigateToCalendarManagement() { + await testSubjects.click('mlCalendarsMngButton'); + await testSubjects.existOrFail('mlPageCalendarManagement'); + }, + + async navigateToFilterListsManagement() { + await testSubjects.click('mlFilterListsMngButton'); + await testSubjects.existOrFail('mlPageFilterListManagement'); + }, }; } diff --git a/x-pack/test/functional/services/ml/settings_calendar.ts b/x-pack/test/functional/services/ml/settings_calendar.ts new file mode 100644 index 0000000000000..34d18c6e12c47 --- /dev/null +++ b/x-pack/test/functional/services/ml/settings_calendar.ts @@ -0,0 +1,196 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningSettingsCalendarProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async parseCalendarTable() { + const table = await testSubjects.find('~mlCalendarTable'); + const $ = await table.parseDomContent(); + const rows = []; + + for (const tr of $.findTestSubjects('~mlCalendarListRow').toArray()) { + const $tr = $(tr); + + rows.push({ + id: $tr + .findTestSubject('mlCalendarListColumnId') + .find('.euiTableCellContent') + .text() + .trim(), + jobs: $tr + .findTestSubject('mlCalendarListColumnJobs') + .find('.euiTableCellContent') + .text() + .trim(), + events: $tr + .findTestSubject('mlCalendarListColumnEvents') + .find('.euiTableCellContent') + .text() + .trim(), + }); + } + + return rows; + }, + + rowSelector(calendarId: string, subSelector?: string) { + const row = `~mlCalendarTable > ~row-${calendarId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + }, + + async filterWithSearchString(filter: string, expectedRowCount: number = 1) { + const tableListContainer = await testSubjects.find('mlCalendarTableContainer'); + const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); + await searchBarInput.clearValueWithKeyboard(); + await searchBarInput.type(filter); + + const rows = await this.parseCalendarTable(); + const filteredRows = rows.filter((row) => row.id === filter); + expect(filteredRows).to.have.length( + expectedRowCount, + `Filtered calendar table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + ); + }, + + async isCalendarRowSelected(calendarId: string): Promise { + return await testSubjects.isChecked( + this.rowSelector(calendarId, `checkboxSelectRow-${calendarId}`) + ); + }, + + async assertCalendarRowSelected(calendarId: string, expectedValue: boolean) { + const isSelected = await this.isCalendarRowSelected(calendarId); + expect(isSelected).to.eql( + expectedValue, + `Expected calendar row for calendar '${calendarId}' to be '${ + expectedValue ? 'selected' : 'deselected' + }' (got '${isSelected ? 'selected' : 'deselected'}')` + ); + }, + + async selectCalendarRow(calendarId: string) { + if ((await this.isCalendarRowSelected(calendarId)) === false) { + await testSubjects.click(this.rowSelector(calendarId, `checkboxSelectRow-${calendarId}`)); + } + + await this.assertCalendarRowSelected(calendarId, true); + }, + + async deselectCalendarRow(calendarId: string) { + if ((await this.isCalendarRowSelected(calendarId)) === true) { + await testSubjects.click(this.rowSelector(calendarId, `checkboxSelectRow-${calendarId}`)); + } + + await this.assertCalendarRowSelected(calendarId, false); + }, + + async assertCreateCalendarButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarButtonCreate'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "create calendar" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertDeleteCalendarButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarButtonDelete'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "delete calendar" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async openCalendarEditForm(calendarId: string) { + await testSubjects.click(this.rowSelector(calendarId, 'mlEditCalendarLink')); + await testSubjects.existOrFail('mlPageCalendarEdit'); + }, + + async assertApplyToAllJobsSwitchEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarApplyToAllJobsSwitch'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "apply calendar to all jobs" switch to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertJobSelectionEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + 'mlCalendarJobSelection > comboBoxToggleListButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "job" selection to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertJobGroupSelectionEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + 'mlCalendarJobGroupSelection > comboBoxToggleListButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "job group" selection to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertNewEventButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarNewEventButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "new event" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertImportEventsButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarImportEventsButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "imports events" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + eventRowSelector(eventDescription: string, subSelector?: string) { + const row = `~mlCalendarEventsTable > ~row-${eventDescription}`; + return !subSelector ? row : `${row} > ${subSelector}`; + }, + + async assertEventRowExists(eventDescription: string) { + await testSubjects.existOrFail(this.eventRowSelector(eventDescription)); + }, + + async assertDeleteEventButtonEnabled(eventDescription: string, expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + this.eventRowSelector(eventDescription, 'mlCalendarEventDeleteButton') + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "delete event" button for event '${eventDescription}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/settings_filter_list.ts b/x-pack/test/functional/services/ml/settings_filter_list.ts new file mode 100644 index 0000000000000..fb1f203b65562 --- /dev/null +++ b/x-pack/test/functional/services/ml/settings_filter_list.ts @@ -0,0 +1,200 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningSettingsFilterListProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async parseFilterListTable() { + const table = await testSubjects.find('~mlFilterListsTable'); + const $ = await table.parseDomContent(); + const rows = []; + + for (const tr of $.findTestSubjects('~mlCalendarListRow').toArray()) { + const $tr = $(tr); + + const inUseSubject = $tr + .findTestSubject('mlFilterListColumnInUse') + .findTestSubject('~mlFilterListUsedByIcon') + .attr('data-test-subj'); + const inUseString = inUseSubject.split(' ')[1]; + const inUse = inUseString === 'inUse' ? true : false; + + rows.push({ + id: $tr + .findTestSubject('mlFilterListColumnId') + .find('.euiTableCellContent') + .text() + .trim(), + description: $tr + .findTestSubject('mlFilterListColumnDescription') + .find('.euiTableCellContent') + .text() + .trim(), + itemCount: $tr + .findTestSubject('mlFilterListColumnItemCount') + .find('.euiTableCellContent') + .text() + .trim(), + inUse, + }); + } + + return rows; + }, + + rowSelector(filterId: string, subSelector?: string) { + const row = `~mlFilterListsTable > ~row-${filterId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + }, + + async filterWithSearchString(filter: string, expectedRowCount: number = 1) { + const tableListContainer = await testSubjects.find('mlFilterListTableContainer'); + const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); + await searchBarInput.clearValueWithKeyboard(); + await searchBarInput.type(filter); + + const rows = await this.parseFilterListTable(); + const filteredRows = rows.filter((row) => row.id === filter); + expect(filteredRows).to.have.length( + expectedRowCount, + `Filtered filter list table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + ); + }, + + async isFilterListRowSelected(filterId: string): Promise { + return await testSubjects.isChecked( + this.rowSelector(filterId, `checkboxSelectRow-${filterId}`) + ); + }, + + async assertFilterListRowSelected(filterId: string, expectedValue: boolean) { + const isSelected = await this.isFilterListRowSelected(filterId); + expect(isSelected).to.eql( + expectedValue, + `Expected filter list row for filter list '${filterId}' to be '${ + expectedValue ? 'selected' : 'deselected' + }' (got '${isSelected ? 'selected' : 'deselected'}')` + ); + }, + + async selectFilterListRow(filterId: string) { + if ((await this.isFilterListRowSelected(filterId)) === false) { + await testSubjects.click(this.rowSelector(filterId, `checkboxSelectRow-${filterId}`)); + } + + await this.assertFilterListRowSelected(filterId, true); + }, + + async deselectFilterListRow(filterId: string) { + if ((await this.isFilterListRowSelected(filterId)) === true) { + await testSubjects.click(this.rowSelector(filterId, `checkboxSelectRow-${filterId}`)); + } + + await this.assertFilterListRowSelected(filterId, false); + }, + + async assertCreateFilterListButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListsButtonCreate'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "create filter list" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertDeleteFilterListButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListsDeleteButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "delete filter list" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async openFilterListEditForm(filterId: string) { + await testSubjects.click(this.rowSelector(filterId, 'mlEditFilterListLink')); + await testSubjects.existOrFail('mlPageFilterListEdit'); + }, + + async assertEditDescriptionButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListEditDescriptionButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "edit filter list description" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertAddItemButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListAddItemButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "add item" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertDeleteItemButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListDeleteItemButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "delete item" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + filterItemSelector(filterItem: string, subSelector?: string) { + const row = `mlGridItem ${filterItem}`; + return !subSelector ? row : `${row} > ${subSelector}`; + }, + + async assertFilterItemExists(filterItem: string) { + await testSubjects.existOrFail(this.filterItemSelector(filterItem)); + }, + + async isFilterItemSelected(filterItem: string): Promise { + return await testSubjects.isChecked( + this.filterItemSelector(filterItem, 'mlGridItemCheckbox') + ); + }, + + async assertFilterItemSelected(filterItem: string, expectedValue: boolean) { + const isSelected = await this.isFilterItemSelected(filterItem); + expect(isSelected).to.eql( + expectedValue, + `Expected filter item '${filterItem}' to be '${ + expectedValue ? 'selected' : 'deselected' + }' (got '${isSelected ? 'selected' : 'deselected'}')` + ); + }, + + async selectFilterItem(filterItem: string) { + if ((await this.isFilterItemSelected(filterItem)) === false) { + await testSubjects.click(this.filterItemSelector(filterItem)); + } + + await this.assertFilterItemSelected(filterItem, true); + }, + + async deselectFilterItem(filterItem: string) { + if ((await this.isFilterItemSelected(filterItem)) === true) { + await testSubjects.click(this.filterItemSelector(filterItem)); + } + + await this.assertFilterItemSelected(filterItem, false); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/single_metric_viewer.ts b/x-pack/test/functional/services/ml/single_metric_viewer.ts index a65ac09a0b056..c6b912d83fae6 100644 --- a/x-pack/test/functional/services/ml/single_metric_viewer.ts +++ b/x-pack/test/functional/services/ml/single_metric_viewer.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningSingleMetricViewerProvider({ getService }: FtrProviderContext) { + const comboBox = getService('comboBox'); const testSubjects = getService('testSubjects'); return { @@ -15,12 +16,24 @@ export function MachineLearningSingleMetricViewerProvider({ getService }: FtrPro await testSubjects.existOrFail('mlNoSingleMetricJobsFound'); }, - async assertForecastButtonExistsExsist() { + async assertForecastButtonExists() { await testSubjects.existOrFail( 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast' ); }, + async assertForecastButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "forecast" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async assertDetectorInputExsist() { await testSubjects.existOrFail( 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerDetectorSelect' @@ -46,6 +59,28 @@ export function MachineLearningSingleMetricViewerProvider({ getService }: FtrPro await this.assertDetectorInputValue(detectorOptionValue); }, + async assertEntityInputExsist(entityFieldName: string) { + await testSubjects.existOrFail(`mlSingleMetricViewerEntitySelection ${entityFieldName}`); + }, + + async assertEntityInputSelection(entityFieldName: string, expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + `mlSingleMetricViewerEntitySelection ${entityFieldName} > comboBoxInput` + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected entity field selection for '${entityFieldName}' to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async selectEntityValue(entityFieldName: string, entityFieldValue: string) { + await comboBox.set( + `mlSingleMetricViewerEntitySelection ${entityFieldName} > comboBoxInput`, + entityFieldValue + ); + await this.assertEntityInputSelection(entityFieldName, [entityFieldValue]); + }, + async assertChartExsist() { await testSubjects.existOrFail('mlSingleMetricViewerChart'); }, @@ -55,5 +90,32 @@ export function MachineLearningSingleMetricViewerProvider({ getService }: FtrPro timeout: 30 * 1000, }); }, + + async openForecastModal() { + await testSubjects.click( + 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast' + ); + await testSubjects.existOrFail('mlModalForecast'); + }, + + async closeForecastModal() { + await testSubjects.click('mlModalForecast > mlModalForecastButtonClose'); + await testSubjects.missingOrFail('mlModalForecast'); + }, + + async assertForecastModalRunButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlModalForecast > mlModalForecastButtonRun'); + expect(isEnabled).to.eql( + expectedValue, + `Expected forecast "run" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async openAnomalyExplorer() { + await testSubjects.click('mlAnomalyResultsViewSelectorExplorer'); + await testSubjects.existOrFail('mlPageAnomalyExplorer'); + }, }; } diff --git a/x-pack/test/functional/services/transform/security_ui.ts b/x-pack/test/functional/services/transform/security_ui.ts index 9c167e429941f..ced4625d0eb0e 100644 --- a/x-pack/test/functional/services/transform/security_ui.ts +++ b/x-pack/test/functional/services/transform/security_ui.ts @@ -31,5 +31,9 @@ export function TransformSecurityUIProvider( async loginAsTransformViewer() { await this.loginAs(USER.TRANSFORM_VIEWER); }, + + async logout() { + await PageObjects.security.forceLogout(); + }, }; } diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 37d8b6e51072f..77e52b642261b 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -116,12 +116,19 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('transformListTable loaded', { timeout: 30 * 1000 }); } - public async filterWithSearchString(filter: string) { + public async filterWithSearchString(filter: string, expectedRowCount: number = 1) { await this.waitForTransformsToLoad(); const tableListContainer = await testSubjects.find('transformListTableContainer'); const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); await searchBarInput.clearValueWithKeyboard(); await searchBarInput.type(filter); + + const rows = await this.parseTransformTable(); + const filteredRows = rows.filter((row) => row.id === filter); + expect(filteredRows).to.have.length( + expectedRowCount, + `Filtered DFA job table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + ); } public async assertTransformRowFields(transformId: string, expectedRow: object) { diff --git a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts index b55da0798c5f0..410b0fe093002 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts @@ -14,7 +14,7 @@ export default function ({ getService }: FtrProviderContext) { describe('ingest_manager_agent_policies', () => { describe('POST /api/ingest_manager/agent_policies', () => { it('should work with valid values', async () => { - const { body: apiResponse } = await supertest + await supertest .post(`/api/ingest_manager/agent_policies`) .set('kbn-xsrf', 'xxxx') .send({ @@ -22,8 +22,6 @@ export default function ({ getService }: FtrProviderContext) { namespace: 'default', }) .expect(200); - - expect(apiResponse.success).to.be(true); }); it('should return a 400 with an empty namespace', async () => { @@ -61,7 +59,7 @@ export default function ({ getService }: FtrProviderContext) { it('should work with valid values', async () => { const { - body: { success, item }, + body: { item }, } = await supertest .post(`/api/ingest_manager/agent_policies/${TEST_POLICY_ID}/copy`) .set('kbn-xsrf', 'xxxx') @@ -73,7 +71,6 @@ export default function ({ getService }: FtrProviderContext) { // eslint-disable-next-line @typescript-eslint/naming-convention const { id, updated_at, ...newPolicy } = item; - expect(success).to.be(true); expect(newPolicy).to.eql({ name: 'Copied policy', description: 'Test', diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/acks.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/acks.ts index c9fa80c88762b..1613ca9d11ee6 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/acks.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/acks.ts @@ -90,7 +90,6 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); expect(apiResponse.action).to.be('acks'); - expect(apiResponse.success).to.be(true); const { body: eventResponse } = await supertest .get(`/api/ingest_manager/fleet/agents/agent1/events`) .set('kbn-xsrf', 'xx') diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/actions.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/actions.ts index 8dc4e5c232b80..2b4bb335dfc5c 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/actions.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/actions.ts @@ -33,7 +33,6 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(apiResponse.success).to.be(true); expect(apiResponse.item.data).to.eql({ data: 'action_data' }); expect(apiResponse.item.sent_at).to.be('2020-03-18T19:45:02.620Z'); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/checkin.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/checkin.ts index 79f6cfae175e1..fbfc94a0840b0 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/checkin.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/checkin.ts @@ -102,7 +102,6 @@ export default function (providerContext: FtrProviderContext) { .expect(200); expect(apiResponse.action).to.be('checkin'); - expect(apiResponse.success).to.be(true); }); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/complete_flow.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/complete_flow.ts index 2d14a7a10e668..1d5b682d71c7a 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/complete_flow.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/complete_flow.ts @@ -62,7 +62,6 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(200); - expect(enrollmentResponse.success).to.eql(true); const agentAccessAPIKey = enrollmentResponse.item.access_api_key; @@ -76,14 +75,13 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(checkinApiResponse.success).to.eql(true); expect(checkinApiResponse.actions).length(1); expect(checkinApiResponse.actions[0].type).be('CONFIG_CHANGE'); const policyChangeAction = checkinApiResponse.actions[0]; const defaultOutputApiKey = policyChangeAction.data.config.outputs.default.api_key; // Ack actions - const { body: ackApiResponse } = await supertestWithoutAuth + await supertestWithoutAuth .post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/acks`) .set('Authorization', `ApiKey ${agentAccessAPIKey}`) .set('kbn-xsrf', 'xx') @@ -102,7 +100,6 @@ export default function (providerContext: FtrProviderContext) { ], }) .expect(200); - expect(ackApiResponse.success).to.eql(true); // Second agent checkin const { body: secondCheckinApiResponse } = await supertestWithoutAuth @@ -113,7 +110,6 @@ export default function (providerContext: FtrProviderContext) { events: [], }) .expect(200); - expect(secondCheckinApiResponse.success).to.eql(true); expect(secondCheckinApiResponse.actions).length(0); // Get agent @@ -121,18 +117,16 @@ export default function (providerContext: FtrProviderContext) { .get(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}`) .expect(200); - expect(getAgentApiResponse.success).to.eql(true); expect(getAgentApiResponse.item.packages).to.contain( 'system', "Agent should run the 'system' package" ); // Unenroll agent - const { body: unenrollResponse } = await supertest + await supertest .post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/unenroll`) .set('kbn-xsrf', 'xx') .expect(200); - expect(unenrollResponse.success).to.eql(true); // Checkin after unenrollment const { body: checkinAfterUnenrollResponse } = await supertestWithoutAuth @@ -144,13 +138,12 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(checkinAfterUnenrollResponse.success).to.eql(true); expect(checkinAfterUnenrollResponse.actions).length(1); expect(checkinAfterUnenrollResponse.actions[0].type).be('UNENROLL'); const unenrollAction = checkinAfterUnenrollResponse.actions[0]; // ack unenroll actions - const { body: ackUnenrollApiResponse } = await supertestWithoutAuth + await supertestWithoutAuth .post(`/api/ingest_manager/fleet/agents/${enrollmentResponse.item.id}/acks`) .set('Authorization', `ApiKey ${agentAccessAPIKey}`) .set('kbn-xsrf', 'xx') @@ -168,7 +161,6 @@ export default function (providerContext: FtrProviderContext) { ], }) .expect(200); - expect(ackUnenrollApiResponse.success).to.eql(true); // Checkin after unenrollment acknowledged await supertestWithoutAuth diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/delete.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/delete.ts index dc05b7a4dd792..65fbdabe8bd79 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/delete.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/delete.ts @@ -68,7 +68,6 @@ export default function ({ getService }: FtrProviderContext) { .expect(404); expect(apiResponse).not.to.eql({ - success: true, action: 'deleted', }); }); @@ -88,7 +87,6 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xx') .expect(200); expect(apiResponse).to.eql({ - success: true, action: 'deleted', }); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/enroll.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/enroll.ts index 93f656ea952d2..eef0d3beb69d0 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/enroll.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/enroll.ts @@ -136,7 +136,6 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(200); - expect(apiResponse.success).to.eql(true); expect(apiResponse.item).to.have.keys('id', 'active', 'access_api_key', 'type', 'policy_id'); }); @@ -158,7 +157,7 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(200); - expect(apiResponse.success).to.eql(true); + const { body: privileges } = await getEsClientForAPIKey( providerContext, apiResponse.item.access_api_key diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/list.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/list.ts index 23563c6f43bbe..1ee00ed720169 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/list.ts @@ -78,8 +78,7 @@ export default function ({ getService }: FtrProviderContext) { .auth(users.fleet_admin.username, users.fleet_admin.password) .expect(200); - expect(apiResponse).to.have.keys('success', 'page', 'total', 'list'); - expect(apiResponse.success).to.eql(true); + expect(apiResponse).to.have.keys('page', 'total', 'list'); expect(apiResponse.total).to.eql(4); }); it('should return the list of agents when requesting as a user with fleet read permissions', async () => { @@ -87,8 +86,7 @@ export default function ({ getService }: FtrProviderContext) { .get(`/api/ingest_manager/fleet/agents`) .auth(users.fleet_user.username, users.fleet_user.password) .expect(200); - expect(apiResponse).to.have.keys('success', 'page', 'total', 'list'); - expect(apiResponse.success).to.eql(true); + expect(apiResponse).to.have.keys('page', 'total', 'list'); expect(apiResponse.total).to.eql(4); }); it('should not return the list of agents when requesting as a user without fleet permissions', async () => { diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts index d1ff8731183ba..d24e438fa13ea 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts @@ -65,20 +65,17 @@ export default function (providerContext: FtrProviderContext) { }); it('allow to unenroll using a list of ids', async () => { - const { body } = await supertest + await supertest .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') .send({ force: true, }) .expect(200); - - expect(body).to.have.keys('success'); - expect(body.success).to.be(true); }); it('should invalidate related API keys', async () => { - const { body } = await supertest + await supertest .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') .send({ @@ -86,9 +83,6 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(body).to.have.keys('success'); - expect(body.success).to.be(true); - const { body: { api_keys: accessAPIKeys }, } = await esClient.security.getApiKey({ id: accessAPIKeyId }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts index 275462072919b..6c5d552a51eb9 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts @@ -67,13 +67,11 @@ export default function (providerContext: FtrProviderContext) { }); it('should invalide an existing api keys', async () => { - const { body: apiResponse } = await supertest + await supertest .delete(`/api/ingest_manager/fleet/enrollment-api-keys/${keyId}`) .set('kbn-xsrf', 'xxx') .expect(200); - expect(apiResponse.success).to.eql(true); - const { body: { api_keys: apiKeys }, } = await es.security.getApiKey({ id: esApiKeyId }); @@ -113,7 +111,6 @@ export default function (providerContext: FtrProviderContext) { }) .expect(200); - expect(apiResponse.success).to.eql(true); expect(apiResponse.item).to.have.keys('id', 'api_key', 'api_key_id', 'name', 'policy_id'); }); @@ -125,7 +122,7 @@ export default function (providerContext: FtrProviderContext) { policy_id: 'policy1', }) .expect(200); - expect(apiResponse.success).to.eql(true); + const { body: privileges } = await getEsClientForAPIKey( providerContext, apiResponse.item.api_key diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index fac8a26fd6aec..7c1ebef337baa 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -22,5 +22,8 @@ export default function ({ loadTestFile }) { // Agent policies loadTestFile(require.resolve('./agent_policy/index')); + + // Settings + loadTestFile(require.resolve('./settings/index')); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_policy/create.ts b/x-pack/test/ingest_manager_api_integration/apis/package_policy/create.ts index c88f03de59615..113fbeca494d8 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/package_policy/create.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { warnAndSkipTest } from '../../helpers'; @@ -34,7 +33,7 @@ export default function ({ getService }: FtrProviderContext) { it('should work with valid values', async function () { if (server.enabled) { - const { body: apiResponse } = await supertest + await supertest .post(`/api/ingest_manager/package_policies`) .set('kbn-xsrf', 'xxxx') .send({ @@ -52,8 +51,6 @@ export default function ({ getService }: FtrProviderContext) { }, }) .expect(200); - - expect(apiResponse.success).to.be(true); } else { warnAndSkipTest(this, log); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_policy/get.ts b/x-pack/test/ingest_manager_api_integration/apis/package_policy/get.ts index 756eaa8ea49ba..53a400d2fd9eb 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/package_policy/get.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/package_policy/get.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -75,11 +74,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should succeed with a valid id', async function () { - const { body: apiResponse } = await supertest - .get(`/api/ingest_manager/package_policies/${packagePolicyId}`) - .expect(200); - - expect(apiResponse.success).to.be(true); + await supertest.get(`/api/ingest_manager/package_policies/${packagePolicyId}`).expect(200); }); it('should return a 404 with an invalid id', async function () { diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_policy/update.ts b/x-pack/test/ingest_manager_api_integration/apis/package_policy/update.ts index e74d88f3538e4..3c4cb93ee3ff3 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/package_policy/update.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/package_policy/update.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; @@ -77,7 +76,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should work with valid values', async function () { - const { body: apiResponse } = await supertest + await supertest .put(`/api/ingest_manager/package_policies/${packagePolicyId}`) .set('kbn-xsrf', 'xxxx') .send({ @@ -95,8 +94,6 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(200); - - expect(apiResponse.success).to.be(true); }); it('should return a 500 if there is another package policy with the same name', async function () { diff --git a/x-pack/test/ingest_manager_api_integration/apis/settings/index.js b/x-pack/test/ingest_manager_api_integration/apis/settings/index.js new file mode 100644 index 0000000000000..99346fcabeff4 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/settings/index.js @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function loadTests({ loadTestFile }) { + describe('Settings Endpoints', () => { + loadTestFile(require.resolve('./update')); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts b/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts new file mode 100644 index 0000000000000..86292b535db2d --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/settings/update.ts @@ -0,0 +1,53 @@ +/* + * 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 { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + + describe('Settings - update', async function () { + skipIfNoDockerRegistry(providerContext); + + it("should bump all agent policy's revision", async function () { + const { body: testPolicy1PostRes } = await supertest + .post(`/api/ingest_manager/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'test', + description: '', + namespace: 'default', + }); + const { body: testPolicy2PostRes } = await supertest + .post(`/api/ingest_manager/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'test2', + description: '', + namespace: 'default', + }); + await supertest + .put(`/api/ingest_manager/settings`) + .set('kbn-xsrf', 'xxxx') + .send({ kibana_urls: ['http://localhost:1232/abc', 'http://localhost:1232/abc'] }); + + const getTestPolicy1Res = await kibanaServer.savedObjects.get({ + type: 'ingest-agent-policies', + id: testPolicy1PostRes.item.id, + }); + const getTestPolicy2Res = await kibanaServer.savedObjects.get({ + type: 'ingest-agent-policies', + id: testPolicy2PostRes.item.id, + }); + expect(getTestPolicy1Res.attributes.revision).equal(2); + expect(getTestPolicy2Res.attributes.revision).equal(2); + }); + }); +} diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx index 4afd71fd67a69..f3d1eb60bf1c0 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -82,6 +82,7 @@ const AppRoot = React.memo( diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts index 0145ca2a18092..6a68bd530cf63 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts @@ -27,7 +27,8 @@ export default function ({ getService }: FtrProviderContext) { ); }; - describe('Exports from Non-default Space', () => { + // FLAKY: https://github.com/elastic/kibana/issues/76551 + describe.skip('Exports from Non-default Space', () => { before(async () => { await esArchiver.load('reporting/ecommerce'); await esArchiver.load('reporting/ecommerce_kibana_spaces'); // dashboard in non default space diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts index feda5c1386e98..49db8696c1134 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts @@ -21,7 +21,8 @@ export default function ({ getService }: FtrProviderContext) { const reportingAPI = getService('reportingAPI'); const usageAPI = getService('usageAPI'); - describe('Usage', () => { + // FAILING: https://github.com/elastic/kibana/issues/76581 + describe.skip('Usage', () => { before(async () => { await esArchiver.load(OSS_KIBANA_ARCHIVE_PATH); await esArchiver.load(OSS_DATA_ARCHIVE_PATH); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index a0998f1a838ba..325283f5e3440 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import Url from 'url'; import { FtrProviderContext } from '../../ftr_provider_context'; import { PolicyTestResourceInfo } from '../../services/endpoint_policy'; @@ -18,6 +19,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ]); const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); + const config = getService('config'); + const kbnTestServer = config.get('servers.kibana'); + const { protocol, hostname, port } = kbnTestServer; + + const kibanaUrl = Url.format({ + protocol, + hostname, + port, + }); describe('When on the Endpoint Policy Details Page', function () { this.tags(['ciGroup7']); @@ -108,6 +118,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { inputs: [ { id: policyInfo.packagePolicy.id, + revision: 2, data_stream: { namespace: 'default' }, name: 'Protect East Coast', meta: { @@ -222,6 +233,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { type: 'elasticsearch', }, }, + fleet: { + kibana: { + hosts: [kibanaUrl], + }, + }, revision: 3, agent: { monitoring: { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts index d5106f5549924..17a4182fe9371 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts @@ -54,7 +54,6 @@ export default function (providerContext: FtrProviderContext) { }, }) .expect(200); - expect(enrollmentResponse.success).to.eql(true); agentAccessAPIKey = enrollmentResponse.item.access_api_key; }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts new file mode 100644 index 0000000000000..7fbba4e04798d --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts @@ -0,0 +1,57 @@ +/* + * 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 { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; +import { ResolverEntityIndex } from '../../../../plugins/security_solution/common/endpoint/types'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('Resolver tests for the entity route', () => { + before(async () => { + await esArchiver.load('endpoint/resolver/signals'); + }); + + after(async () => { + await esArchiver.unload('endpoint/resolver/signals'); + }); + + it('returns an event even if it does not have a mapping for entity_id', async () => { + // this id is from the es archive + const _id = 'fa7eb1546f44fd47d8868be8d74e0082e19f22df493c67a7725457978eb648ab'; + const { body }: { body: ResolverEntityIndex } = await supertest.get( + `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default` + ); + expect(body).eql([ + { + // this value is from the es archive + entity_id: + 'MTIwNWY1NWQtODRkYS00MzkxLWIyNWQtYTNkNGJmNDBmY2E1LTc1NTItMTMyNDM1NDY1MTQuNjI0MjgxMDA=', + }, + ]); + }); + + it('does not return an event when it does not have the entity_id field in the document', async () => { + // this id is from the es archive + const _id = 'no-entity-id-field'; + const { body }: { body: ResolverEntityIndex } = await supertest.get( + `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default` + ); + expect(body).to.be.empty(); + }); + + it('does not return an event when it does not have the process field in the document', async () => { + // this id is from the es archive + const _id = 'no-process-field'; + const { body }: { body: ResolverEntityIndex } = await supertest.get( + `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default` + ); + expect(body).to.be.empty(); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts index fc603af3619a4..ecfc1ef5bb7f5 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts @@ -10,6 +10,7 @@ export default function (providerContext: FtrProviderContext) { describe('Resolver tests', () => { loadTestFile(require.resolve('./entity_id')); + loadTestFile(require.resolve('./entity')); loadTestFile(require.resolve('./children')); loadTestFile(require.resolve('./tree')); loadTestFile(require.resolve('./alerts')); diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index df1d7e789507b..d0e2ea952e108 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -1,6 +1,7 @@ { - "extends": "../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/x-pack/test", "types": [ "mocha", "node", diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 7c6210bb9ce19..b5080f83f2eda 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.json", + "extends": "../tsconfig.base.json", "include": [ "mocks.ts", "typings/**/*", @@ -33,12 +33,7 @@ "plugins/*": ["src/legacy/core_plugins/*/public/"], "fixtures/*": ["src/fixtures/*"], }, - "types": [ - "node", - "jest", - "flot", - "jest-styled-components", - "@testing-library/jest-dom" - ] + // overhead is too significant + "incremental": false, } } diff --git a/yarn.lock b/yarn.lock index 3be2599c64201..95066c9fa8cda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1055,7 +1055,7 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@cypress/listr-verbose-renderer@0.4.1": +"@cypress/listr-verbose-renderer@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" integrity sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo= @@ -1065,7 +1065,7 @@ date-fns "^1.27.2" figures "^1.7.0" -"@cypress/request@2.88.5": +"@cypress/request@^2.88.5": version "2.88.5" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== @@ -1103,7 +1103,7 @@ "@babel/preset-env" "^7.0.0" babel-loader "^8.0.2" -"@cypress/xvfb@1.2.4": +"@cypress/xvfb@^1.2.4": version "1.2.4" resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== @@ -4268,10 +4268,10 @@ dependencies: "@types/node" "*" -"@types/node-forge@^0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.9.0.tgz#e9f678ec09283f9f35cb8de6c01f86be9278ac08" - integrity sha512-J00+BIHJOfagO1Qs67Jp5CZO3VkFxY8YKMt44oBhXr+3ZYNnl8wv/vtcJyPjuH0QZ+q7+5nnc6o/YH91ZJy2pQ== +"@types/node-forge@^0.9.5": + version "0.9.5" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.9.5.tgz#648231d79da197216290429020698d4e767365a0" + integrity sha512-rrN3xfA/oZIzwOnO3d2wRQz7UdeVkmMMPjWUCfpPTPuKFVb3D6G10LuiVHYYmvrivBBLMx4m0P/FICoDbNZUMA== dependencies: "@types/node" "*" @@ -4663,12 +4663,12 @@ resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.13.tgz#ca039c23a9e27ebea53e0901ef928ea2a1a6d313" integrity sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung== -"@types/sinonjs__fake-timers@6.0.1": +"@types/sinonjs__fake-timers@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== -"@types/sizzle@*", "@types/sizzle@2.3.2": +"@types/sizzle@*", "@types/sizzle@^2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== @@ -5590,7 +5590,7 @@ ajv@^4.7.0: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5, ajv@^6.9.1: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.5.5, ajv@^6.9.1: version "6.12.4" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== @@ -6128,7 +6128,7 @@ aproba@^1.0.3, aproba@^1.1.1: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -arch@2.1.2: +arch@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf" integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ== @@ -6590,6 +6590,11 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + atob-lite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696" @@ -7387,6 +7392,11 @@ bl@^4.0.1: inherits "^2.0.4" readable-stream "^3.4.0" +blob-util@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -7409,7 +7419,7 @@ bluebird@3.5.5, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== -bluebird@3.7.2: +bluebird@3.7.2, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -7791,15 +7801,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.1.0, buffer@^5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6" - integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - -buffer@^5.5.0: +buffer@^5.1.0, buffer@^5.2.0, buffer@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== @@ -7962,7 +7964,7 @@ cacheable-request@^7.0.1: normalize-url "^4.1.0" responselike "^2.0.0" -cachedir@2.3.0: +cachedir@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== @@ -8345,7 +8347,7 @@ check-disk-space@^2.1.0: resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-2.1.0.tgz#2e77fe62f30d9676dc37a524ea2008f40c780295" integrity sha512-f0nx9oJF/AVF8nhSYlF1EBvMNnO+CXyLwKhPvN1943iOMI9TWhQigLZm80jAf0wzQhwKkzA8XXjyvuVUeGGcVQ== -check-more-types@2.24.0: +check-more-types@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= @@ -8651,6 +8653,16 @@ cli-table3@0.5.1: optionalDependencies: colors "^1.1.2" +cli-table3@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" + integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== + dependencies: + object-assign "^4.1.0" + string-width "^4.2.0" + optionalDependencies: + colors "^1.1.2" + cli-table@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" @@ -9050,11 +9062,6 @@ commander@3.0.2: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== -commander@4.1.1, commander@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" @@ -9075,12 +9082,17 @@ commander@^3.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.0.tgz#0641ea00838c7a964627f04cddc336a2deddd60a" integrity sha512-pl3QrGOBa9RZaslQiqnnKX2J068wcQw7j9AIaBQ9/JEp5RY6je4jKTImg0Bd+rpoONSe7GUFSgkxLeo17m3Pow== +commander@^4.0.1, commander@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + commander@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.0.0.tgz#dbf1909b49e5044f8fdaf0adc809f0c0722bdfd0" integrity sha512-JrDGPAKjMGSP1G0DUoaceEJ3DZgAfr/q6X7FVk4+U5KxUSKviYGM2k6zWkfyyBHy5rAtzgYJFa1ro2O9PtoxwQ== -common-tags@1.8.0: +common-tags@1.8.0, common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== @@ -9954,48 +9966,56 @@ cypress-multi-reporters@^1.2.3: debug "^4.1.1" lodash "^4.17.11" -cypress@4.11.0: - version "4.11.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.11.0.tgz#054b0b85fd3aea793f186249ee1216126d5f0a7e" - integrity sha512-6Yd598+KPATM+dU1Ig0g2hbA+R/o1MAKt0xIejw4nZBVLSplCouBzqeKve6XsxGU6n4HMSt/+QYsWfFcoQeSEw== - dependencies: - "@cypress/listr-verbose-renderer" "0.4.1" - "@cypress/request" "2.88.5" - "@cypress/xvfb" "1.2.4" - "@types/sinonjs__fake-timers" "6.0.1" - "@types/sizzle" "2.3.2" - arch "2.1.2" - bluebird "3.7.2" - cachedir "2.3.0" - chalk "2.4.2" - check-more-types "2.24.0" - cli-table3 "0.5.1" - commander "4.1.1" - common-tags "1.8.0" - debug "4.1.1" - eventemitter2 "6.4.2" - execa "1.0.0" - executable "4.1.1" - extract-zip "1.7.0" - fs-extra "8.1.0" - getos "3.2.1" - is-ci "2.0.0" - is-installed-globally "0.3.2" - lazy-ass "1.6.0" - listr "0.14.3" - lodash "4.17.19" - log-symbols "3.0.0" - minimist "1.2.5" - moment "2.26.0" - ospath "1.2.2" - pretty-bytes "5.3.0" - ramda "0.26.1" - request-progress "3.0.0" - supports-color "7.1.0" - tmp "0.1.0" - untildify "4.0.0" - url "0.11.0" - yauzl "2.10.0" +cypress@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.0.0.tgz#6957e299b790af8b1cd7bea68261b8935646f72e" + integrity sha512-jhPd0PMO1dPSBNpx6pHVLkmnnaTfMy3wCoacHAKJ9LJG06y16zqUNSFri64N4BjuGe8y6mNMt8TdgKnmy9muCg== + dependencies: + "@cypress/listr-verbose-renderer" "^0.4.1" + "@cypress/request" "^2.88.5" + "@cypress/xvfb" "^1.2.4" + "@types/sinonjs__fake-timers" "^6.0.1" + "@types/sizzle" "^2.3.2" + arch "^2.1.2" + blob-util "2.0.2" + bluebird "^3.7.2" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-table3 "~0.6.0" + commander "^4.1.1" + common-tags "^1.8.0" + debug "^4.1.1" + eventemitter2 "^6.4.2" + execa "^4.0.2" + executable "^4.1.1" + extract-zip "^1.7.0" + fs-extra "^9.0.1" + getos "^3.2.1" + is-ci "^2.0.0" + is-installed-globally "^0.3.2" + lazy-ass "^1.6.0" + listr "^0.14.3" + lodash "^4.17.19" + log-symbols "^4.0.0" + minimist "^1.2.5" + moment "^2.27.0" + ospath "^1.2.2" + pretty-bytes "^5.3.0" + ramda "~0.26.1" + request-progress "^3.0.0" + supports-color "^7.1.0" + tmp "~0.2.1" + untildify "^4.0.0" + url "^0.11.0" + yauzl "^2.10.0" + +cytoscape-dagre@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/cytoscape-dagre/-/cytoscape-dagre-2.2.2.tgz#5f32a85c0ba835f167efee531df9e89ac58ff411" + integrity sha512-zsg36qNwua/L2stJSWkcbSDcvW3E6VZf6KRe6aLnQJxuXuz89tMqI5EVYVKEcNBgzTEzFMFv0PE3T0nD4m6VDw== + dependencies: + dagre "^0.8.2" cytoscape@^3.10.0: version "3.10.0" @@ -10209,6 +10229,14 @@ d@1: dependencies: es5-ext "^0.10.9" +dagre@^0.8.2: + version "0.8.5" + resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee" + integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw== + dependencies: + graphlib "^2.1.8" + lodash "^4.17.15" + damerau-levenshtein@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514" @@ -10303,7 +10331,7 @@ debug@3.2.6, debug@3.X, debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, dependencies: ms "^2.1.1" -debug@4, debug@4.1.1, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -11277,11 +11305,6 @@ elastic-apm-node@^3.7.0: traceparent "^1.0.0" unicode-byte-truncate "^1.0.0" -elasticsearch-browser@^16.7.0: - version "16.7.0" - resolved "https://registry.yarnpkg.com/elasticsearch-browser/-/elasticsearch-browser-16.7.0.tgz#1f32a402cd36a9bb14a9ea6cb70f8e126d4cb9b1" - integrity sha512-UES2Fbnzy4Ivq4QvES4sfk/a5UytJczeJdfxRWa4kuHEllKOffKQLTxJ8Ti86OREpACQxppqvYgzctJuEiIr7Q== - elasticsearch@^16.4.0: version "16.5.0" resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-16.5.0.tgz#619a48040be25d345fdddf09fa6042a88c3974d6" @@ -11400,14 +11423,7 @@ encoding@^0.1.11: dependencies: iconv-lite "~0.4.13" -end-of-stream@^1.0.0, end-of-stream@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" - integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== - dependencies: - once "^1.4.0" - -end-of-stream@^1.4.1, end-of-stream@^1.4.4: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -12269,10 +12285,10 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter2@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.2.tgz#f31f8b99d45245f0edbc5b00797830ff3b388970" - integrity sha512-r/Pwupa5RIzxIHbEKCkNXqpEQIIT4uQDxmP4G/Lug/NokVUWj0joz/WzWl3OxRpC5kDrH/WdiUJoR+IrwvXJEw== +eventemitter2@^6.4.2: + version "6.4.3" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820" + integrity sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ== eventemitter2@~0.4.13: version "0.4.14" @@ -12319,19 +12335,6 @@ exec-sh@^0.3.2: resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== -execa@1.0.0, execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-0.1.1.tgz#b09c2a9309bc0ef0501479472db3180f8d4c3edd" @@ -12392,6 +12395,19 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + execa@^3.2.0: version "3.4.0" resolved "https://registry.yarnpkg.com/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89" @@ -12430,7 +12446,7 @@ execall@^1.0.0: dependencies: clone-regexp "^1.0.0" -executable@4.1.1: +executable@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== @@ -13468,15 +13484,6 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@8.1.0, fs-extra@^8.0.1, fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-extra@^0.30.0: version "0.30.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" @@ -13506,6 +13513,25 @@ fs-extra@^7.0.0, fs-extra@^7.0.1, fs-extra@~7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^8.0.1, fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" + integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^1.0.0" + fs-minipass@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" @@ -13790,7 +13816,7 @@ getopts@^2.2.5: resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA== -getos@3.2.1, getos@^3.1.0: +getos@^3.1.0, getos@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== @@ -14374,6 +14400,13 @@ graceful-fs@~1.1: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.1.14.tgz#07078db5f6377f6321fceaaedf497de124dc9465" integrity sha1-BweNtfY3f2Mh/Oqu30l94STclGU= +graphlib@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da" + integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A== + dependencies: + lodash "^4.17.15" + graphql-anywhere@^4.1.0-alpha.0: version "4.1.16" resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.16.tgz#82bb59643e30183cfb7b485ed4262a7b39d8a6c1" @@ -16298,13 +16331,6 @@ is-callable@^1.2.0: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== -is-ci@2.0.0, is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - is-ci@^1.0.10: version "1.1.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.1.0.tgz#247e4162e7860cebbdaf30b774d6b0ac7dcfe7a5" @@ -16312,6 +16338,13 @@ is-ci@^1.0.10: dependencies: ci-info "^1.0.0" +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -16478,14 +16511,6 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69" integrity sha1-bghLvJIGH7sJcexYts5tQE4k2mk= -is-installed-globally@0.3.2, is-installed-globally@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" - integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== - dependencies: - global-dirs "^2.0.1" - is-path-inside "^3.0.1" - is-installed-globally@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" @@ -16494,6 +16519,14 @@ is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" +is-installed-globally@^0.3.1, is-installed-globally@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + is-integer@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c" @@ -18001,6 +18034,15 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" + integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== + dependencies: + universalify "^1.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -18302,7 +18344,7 @@ latest-version@^5.0.0: dependencies: package-json "^6.3.0" -lazy-ass@1.6.0: +lazy-ass@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= @@ -18535,7 +18577,7 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" -listr@0.14.3: +listr@0.14.3, listr@^0.14.3: version "0.14.3" resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== @@ -18984,7 +19026,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.11, lodash@4.17.15, lodash@4.17.19, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: +lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -19915,7 +19957,7 @@ minimist@1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= -minimist@1.2.5, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.0: +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.0: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -20182,16 +20224,16 @@ moment-timezone@^0.5.27: dependencies: moment ">= 2.9.0" -moment@2.26.0: - version "2.26.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" - integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== - "moment@>= 2.9.0", moment@>=1.6.0, moment@>=2.14.0, moment@^2.10.6, moment@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== +moment@^2.27.0: + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + monaco-editor@~0.17.0: version "0.17.1" resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.17.1.tgz#8fbe96ca54bfa75262706e044f8f780e904aa45c" @@ -20589,15 +20631,10 @@ node-forge@0.9.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== -node-forge@^0.7.6: - version "0.7.6" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" - integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== - -node-forge@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" - integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== +node-forge@^0.10.0, node-forge@^0.7.6: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== node-gyp@^3.8.0: version "3.8.0" @@ -21584,7 +21621,7 @@ osenv@0, osenv@^0.1.0, osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -ospath@1.2.2: +ospath@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= @@ -22643,16 +22680,16 @@ prettier@^2.1.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6" integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw== -pretty-bytes@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" - integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== - pretty-bytes@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" integrity sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk= +pretty-bytes@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" + integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== + pretty-error@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" @@ -23145,16 +23182,16 @@ railroad-diagrams@^1.0.0: resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= -ramda@0.26.1, ramda@^0.26, ramda@^0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" - integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== - ramda@^0.21.0: version "0.21.0" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35" integrity sha1-oAGr7bP/YQd9T/HVd9RN536NCjU= +ramda@^0.26, ramda@^0.26.1, ramda@~0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" + integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== + randexp@0.4.6: version "0.4.6" resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" @@ -24187,7 +24224,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -"readable-stream@1 || 2", readable-stream@^2.3.7, readable-stream@~2.3.3: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.3, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -24210,29 +24247,7 @@ readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0": isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" - integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.2: +"readable-stream@2 || 3", readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -24891,7 +24906,7 @@ replace-homedir@^1.0.0: is-absolute "^1.0.0" remove-trailing-separator "^1.1.0" -request-progress@3.0.0: +request-progress@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= @@ -27221,13 +27236,6 @@ supports-color@6.1.0, supports-color@^6.1.0: dependencies: has-flag "^3.0.0" -supports-color@7.1.0, supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== - dependencies: - has-flag "^4.0.0" - supports-color@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" @@ -27266,6 +27274,13 @@ supports-color@^7.0.0: dependencies: has-flag "^4.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + supports-hyperlinks@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz#71daedf36cc1060ac5100c351bb3da48c29c0ef7" @@ -27909,6 +27924,13 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" @@ -28797,6 +28819,11 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" + integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== + unlazy-loader@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/unlazy-loader/-/unlazy-loader-0.1.3.tgz#2efdf05c489da311055586bf3cfca0c541dd8fa5" @@ -28837,11 +28864,6 @@ unstated@^2.1.1: dependencies: create-react-context "^0.1.5" -untildify@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" - integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== - untildify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0" @@ -28854,6 +28876,11 @@ untildify@^3.0.3: resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA== +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + unzip-response@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" @@ -29001,7 +29028,7 @@ url-to-options@^1.0.1: resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= -url@0.11.0, url@^0.11.0: +url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=